今回はUnityのカメラ内にゲームオブジェクトがある時だけ何らかの処理を実行してみたいと思います。
MonoBehaviourクラスで定義されているメソッドを使うだけなので簡単に出来ます。(^^)/
今回は簡単なサンプルとしてヒエラルキーにCubeを作成し、そのCubeがカメラの範囲内にある時だけ時間を進めていき、カメラの範囲外に出たら時間の計測をやめるようにしてみます。
今回の機能を作成すると、
上のようにカメラの範囲内の時は時間を計測し、範囲外に出たら計測をやめます。
ただ、注意が必要なのが2点あり、Unityエディター上だとシーンビューでもOnBecameInvisibleやOnBecameVisibleが呼ばれる為、シーンビューを表示した状態の場合はシーンビューを見ているカメラの範囲が使われてしまいます。
なので、確認する時はシーンビューを見えないようにして試してください。
もう一つの注意点はカメラから見えている範囲(ゲームビュー)でCubeが見えている時ではなく、カメラの範囲内か範囲外かという点です。
上のように赤い四角で囲ったカメラの範囲で判断されます。
なのでゲームビュー上でCubeが見えていなくてもカメラの範囲に入っていれば時間は計測されます。
最後にカメラの見えている範囲内にいるかどうかで時間計測をする機能を作ってみましたが、こちらは『だいたいは見えていないから時間を止めるか』程度の性能になります。(^_^;)
また実際にカメラに映っていなくてもレンダリングが必要な場合(影など)は見えている状態と判定されます。
カメラの範囲内にいる時だけ時間を計測するサンプルの作成
それではサンプルを作成していきましょう。
ヒエラルキー上で右クリックから3D Object→Cubeを選択しCubeを配置します。
Cubeに新しくVisibleTimeCountScriptという名前のスクリプトを作成し取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class VisibleTimeCountScript : MonoBehaviour { // カメラ内にいるかどうか private bool isInsideCamera; private float elapsedTime; // 経過時間表示テキスト [SerializeField] private Text timeCountText; // Update is called once per frame void Update() { if(isInsideCamera) { elapsedTime += Time.deltaTime; timeCountText.text = elapsedTime.ToString("0:00.00"); } } // カメラから外れた private void OnBecameInvisible() { isInsideCamera = false; } // カメラ内に入った private void OnBecameVisible() { isInsideCamera = true; } } |
isInsideCameraがカメラの範囲内かどうかを判定するフラグで、elapsedTimeが経過時間、timeCountTextは計測した時間を表示するテキストを設定します。
UpdateメソッドではisInsideCameraがtrueの時だけ時間を計測し、その時間をtimeCountTextに表示します。
OnBecameInvisibleメソッドはMonoBehaviourクラスで定義されているメソッドでスクリプトが設定されているレンダラーがカメラの範囲外になった時に呼ばれます。
OnBecameVisibleはスクリプトが設定されているレンダラーがカメラの範囲内に来た時に呼ばれます。
ここで重要なのがOnBecameInvisibleとOnBecameVisibleが記述されたスクリプトが設定されたゲームオブジェクトがレンダラーコンポーネントを持っている必要があります。
Cubeの場合はCube自身がMesh Rendererを持っているのでOnBecameInvisibleとOnBecameVisibleメソッドが呼ばれます。
例えばスタンダードアセットのEthanでOnBecameInvisibleとOnBecameVisibleを呼び出そうと思ったら、Ethan子要素のEthan BodyやEthan GrassesがSkinned MeshRendererを持っていますので、Ethanではなく子要素のEthan Body等にスクリプトを取り付けOnBecameInvisibleやOnBecameVisibleを呼び出す必要があります。
Cubeにスクリプトを取り付けたら、次は時間を表示するTextを作成します。
ヒエラルキー上で右クリックからUI→Textを選択し、TextのPreset Anchorをstretch stretchにし、TextコンポーネントのFont Size、Alignment、Color等を変更します。
これで時間を表示するTextが出来たので、先ほど作ったCubeに取り付けたVisibleTimeCountScriptのインスペクタのtimeCountTextにドラッグ&ドロップして設定します。
これで機能が完成しました。
カメラに写っているかどうかで時間を計測する
先ほど作ったものはカメラの範囲内にCubeがある場合に時間を計測していました。
次はカメラに写っているかどうかで時間を計測するかどうかを決める機能を作ってみます。
最初に言及した通り完璧なものではありません。
VisibleTimeCountScriptをCtrl+Dキーで複製し、スクリプトの名前をVisibleTimeCountScript2とし、クラス名もVisibleTimeCountScript2とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class VisibleTimeCountScript2 : MonoBehaviour { // カメラ内にいるかどうか private bool isInsideCamera; private float elapsedTime; // 経過時間表示テキスト [SerializeField] private Text timeCountText; // ターゲットのゲームオブジェクト [SerializeField] private Transform target; // オブジェクトの横のオフセット値 [SerializeField] private float horizontalOffset = 0.8f; // オブジェクトの縦のオフセット値 [SerializeField] private float verticalOffset = 0.8f; // Update is called once per frame void Update() { // カメラのビューポートの限界位置をオフセット位置を含めて計算 var leftScreenPos = Camera.main.WorldToViewportPoint(target.position + new Vector3(horizontalOffset, 0f, 0f)); var rightScreenPos = Camera.main.WorldToViewportPoint(target.position - new Vector3(horizontalOffset, 0f, 0f)); var topScreenPos = Camera.main.WorldToViewportPoint(target.position - new Vector3(0f, verticalOffset, 0f)); var bottomScreenPos = Camera.main.WorldToViewportPoint(target.position + new Vector3(0f, verticalOffset, 0f)); // ターゲットのゲームオブジェクトが限界位置を越えてなければ計算 if (0f <= leftScreenPos.x && rightScreenPos.x <= 1f && 0f <= bottomScreenPos.y && topScreenPos.y <= 1f ) { elapsedTime += Time.deltaTime; timeCountText.text = elapsedTime.ToString("0:00.00"); } } } |
ターゲットとなるゲームオブジェクトはインスペクタで設定するようにします。
horizontalOffsetとverticalOffsetはターゲットの位置を調整するオフセット値です。
Updateメソッド内ではCamera.main.WorldToViewportPointメソッドを使ってターゲットのゲームオブジェクトにオフセット値を加えてターゲットゲームオブジェクトの位置のカメラのビューポートの位置を求めます。
カメラのビューポートは0~1の間の値が得られX軸は0が左、1が右、Y軸は0が下、1が上になります。
今回はターゲットのゲームオブジェクトにCubeを指定しますが、Cubeの位置はCubeの真ん中になるのでカメラの見える位置+Cubeの半分の長さ分を足した時にCubeが見えなくなります。
実際にはカメラの角度やターゲットゲームオブジェクトの角度があり、そううまく消えてくれるわけでもないのでオフセット値を使って調整します。
ターゲットの位置から上下左右の消える位置を求めカメラのビューポートの範囲内であれば時間計測をしています。
Cubeに設定したVisibleTimeCountScriptスクリプトを無効にし、Main Cameraゲームオブジェクトを非アクティブにします。
スタンダードアセットのFPSController(Assets/StandardAssets/Characters/FirstPersonCharacter/Prefabs/FPSController)をヒエラルキー上にドラッグ&ドロップをして配置します。
FPSContollerの子要素のFPSPersonCharacterにVisibleTimeCountScript2を設定します。
FPSControllerの移動はキーボードのASDWもしくは十字キーで行い、視点の変更はマウスを動かします。
実際に試してみると、
上のような感じになります。
細かくポイントを作り制度を上げる
Camera.main.WorldToViewportPointメソッドを使ってゲームオブジェクトのビューポート位置を調べカメラに写っているかどうかで判定する事が出来ました。
ただ先ほどのやり方だとオフセット値を使って調整というやや面倒なやり方になるのでゲームオブジェクトの子要素にいくつかのポイントを作り、そのポイントのビューポート位置を全て調べカメラ内にいるかどうかを調べるようにしてみます。
VisibleTimeCountScript2をCtrl+Dキーで複製し、名前をVisibleTimeCountScript3とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class VisibleTimeCountScript3 : MonoBehaviour { private float elapsedTime; // 経過時間表示テキスト [SerializeField] private Text timeCountText; // ターゲットのゲームオブジェクト [SerializeField] private List<Transform> targetPoints; // Update is called once per frame void Update() { // カメラ内にオブジェクトがあるかどうか var isInsideCamera = false; // カメラのビューポート位置 Vector2 viewportPoint; // ターゲットポイントがカメラのビューポート内にあるかどうかを調べる foreach (var targetPoint in targetPoints) { // ビューポートの計算 viewportPoint = Camera.main.WorldToViewportPoint(targetPoint.position); if (0f <= viewportPoint.x && viewportPoint.x <= 1f && 0f <= viewportPoint.y && viewportPoint.y <= 1f ) { isInsideCamera = true; break; } } // ターゲットポイントがひとつでもカメラに写っていたら時間を計測 if (isInsideCamera) { elapsedTime += Time.deltaTime; timeCountText.text = elapsedTime.ToString(); } } } |
targetPointsに登録されたターゲット位置の一つ一つのビューポート位置を調べています。
やっている事はVisibleTimeCountScript2と同じです。
今回はCubeではなく人型モデルで試してみます。
スタンダードアセットのEthanをヒエラルキー上に配置し、ボーンの頭、右手、左手、右足、左足の子要素に空のゲームオブジェクトを使ってターゲットとなる位置を作成します。
上の画像では左右の足のボーンの子要素にそれぞれ2点、頭の子要素に1点、上の画像では見えませんが、両手のボーンの子要素に1点ずつターゲットとなる位置を作成しています。
足のボーンの子要素では片方の足に2点のポイントがあるので移動させて足の指の辺りと踵の辺りにします。
頭の子要素は頭のてっぺん、両手は両手の指先に移動させます。
例えば右手のボーンの子要素に作成したターゲット位置は以下のような位置に移動させます。
FirstPersonCharacterにVisibleTimeCountScript3を取り付けます(VisibleTimeCountScript2は無効にします)。
上のようにEthanのボーンの子要素に作成したターゲット位置を設定します。
試しに実行してみると
上のようになりました。
Ethan子要素のターゲット位置を増やせばより細かく調べられそうです。
複数のキャラクターのどれかがカメラ内にいるか判定したい
先ほど作成したEthan子要素にターゲット位置を作り調べる方法で精度が上がりました。
しかし複数のキャラクターを配置し、どれかのキャラクターのターゲット位置が見えていたら時間を計測したい場合は全てのキャラクターのターゲット位置をリストに設定しておかなければいけません。
これでは大変なので、ターゲット位置をボーンの子要素に配置せず、キャラクターを覆うような8つのターゲット位置を作り大まかに計算し、複数のキャラクターを検査するのをやりやすくします。
VisibleTimeCountScript3をCtr+Dキーで複製し、VisibleTimeCountScript4とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class VisibleTimeCountScript4 : MonoBehaviour { private float elapsedTime; // 経過時間表示テキスト [SerializeField] private Text timeCountText; // ターゲットのゲームオブジェクト [SerializeField] private List<Transform> targetList; // Update is called once per frame void Update() { // カメラ内にオブジェクトがあるかどうか var isInsideCamera = false; // カメラのビューポート位置 Vector2 viewportPoint; // ターゲットポイント Transform targetPoints; // ターゲットポイントがカメラのビューポート内にあるかどうかを調べる foreach (var character in targetList) { targetPoints = character.Find("TargetPoints"); for (int i = 0; i < targetPoints.childCount; i++) { // ビューポートの計算 viewportPoint = Camera.main.WorldToViewportPoint(targetPoints.GetChild(i).position); if (0f <= viewportPoint.x && viewportPoint.x <= 1f && 0f <= viewportPoint.y && viewportPoint.y <= 1f ) { isInsideCamera = true; break; } } if(isInsideCamera) { break; } } // ターゲットポイントがひとつでもカメラに写っていたら時間を計測 if (isInsideCamera) { elapsedTime += Time.deltaTime; timeCountText.text = elapsedTime.ToString(); } } } |
targetListにはカメラ位置にいるかどうかを調べるキャラクターを複数設定出来るようにします。
targetList分の繰り返しを行い、そのキャラクターの子要素のTargetPointsというゲームオブジェクトを取得し、その子要素にあるそのキャラクターのターゲット位置群のビューポート位置を調べています。
どれかのキャラクターのターゲット位置がビューポート内にあれば時間計測をします。
2重ループになっているので中のfor文を抜けたらisInsideCameraがtrueだったらそのまま外のforeachも抜けるようにしています。
スタンダードアセットのEthanをヒエラルキー上に配置し、Ethanを選択した状態で右クリックからCreate Emptyを選択し、名前をTargetPointsとします。
TargetPointsを選択した状態で右クリックから3D Object→Cubeを選択し、インスペクタのTransformのScaleのXYZを0.1にします。
ターゲット位置をCreate EmptyではなくCubeで作成したのはその位置をわかりやすくする為です。
Cubeを移動や複製し、キャラクターを覆うような位置に8つのターゲット位置を作成します。
実際に配置すると、
上のような感じでキャラクターを覆うように8つのCubeを配置しました。
FirstPersonCharacterのVisibleTimeCountScript3を削除し、VisibleTimeCountScript4を取り付けます。
Ethanを選択した状態でCtrl+Dキーを押して複製し、少し移動させておきます。
FirstPersonCharacterのVisibleTimeCountScript4のtargetListにカメラ内にいるかどうかを調べたいキャラクターを設定します。
これで機能が出来たので、試してみます。
上のようになりました。
この方法だと複数のゲームオブジェクトを調べたい時に楽になるかなと思います。
ただ計算量は多いので調べるキャラクターが多くなると処理が遅くなるかもしれません。
上の動画だとCubeをそのまま表示していますが、配置し終わったらCubeのそれぞれのインスペクタのTransformコンポーネント以外の物を削除すれば空のゲームオブジェクトと変わりはないのでそうしてください。
この方法の問題点はボーンの子要素にターゲット位置があるわけではないので、アニメーションの動きによっては手や足がキャラクターを囲ったターゲット位置の外へ出てしまってカメラに映っても検知されないという点です。
終わりに
カメラの範囲内かどうかで何らかの処理を実行したい時もあるかな?と思って今回の機能を作成してみました。
カメラから見えているゲームオブジェクトだけ判定するようなプロパティ等もありそうな気がしますが、わたくしは見つけられなかったのでとりあえずこうなりました。(-_-)