今回はUnityでソニーから発売されたホラーゲームSIRENの視界ジャック機能を作成してみたいと思います。
SIRENの視界ジャック機能は屍人の視界(屍人の目線)で周りを見ることが出来、屍人の行動や喋っている言葉等を体感出来る機能です。
わたくしがSIRENを好きなので作ってみました。(^^)/
今回の機能を作成すると、
上のように移動キーを押した方向に一番近い敵の視界になり、敵から見たゲーム世界を見ることが出来ます。
最初はキーボードの方向キーで確認し、途中からPS4コントローラーを接続し確認しました。
PS4コントローラーを接続すると音声がコントローラーヘッドホンの方に流れるようなので音声が消えたようになりました(動画では音声が流れます)。
視界ジャック機能の概要
視界ジャック機能を作る前にまずはどうやって作るか?を考えていきたいと思います。
操作キャラクターは通常通り移動や攻撃等(今回は移動機能だけ作成)が出来るようにして、視界ジャックを行うボタンを押した時に視界ジャックモードに入るとします。
今回はPS4コントローラーの△ボタンを視界ジャックモードに入るボタンに割り当てる事にします(キーボードでも出来ます)。
視界ジャックモードになったらキャラクターの移動や攻撃は出来ないようにし、コントローラーのアナログスティックで周りにいる敵を検索します。
視界ジャック中はテレビの砂嵐動画と音声を再生します。
アナログスティックの入力は横軸と縦軸の入力を3D空間のX軸とZ軸に割り当ててワールド軸の前方からの角度を計算し、敵のいる角度はワールド軸の前方からの角度で計算します。
「ワールド軸の前方から見たアナログスティックの入力角度」と「ワールド軸の前方から見た敵の角度」を比較しより近い敵を視界ジャックする敵とします。
ただしアナログスティックの入力をInput.GetAxisを使って、
1 2 3 | var input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); |
上のようにXZ軸に割り当てた場合は必ずVector3.forward(ワールド軸の前方)が縦軸の入力であるInput.GetAxis(“Vertical”)になってしまいます。
例えばキャラクターを90度回転させてアナログスティックの↑を押したとしてもVector3.forwardの方向の入力になってしまい、キャラクターの前方の入力になりません。
本来であればアナログスティックの↑を押したらキャラクターの前方方向を押したと判定したいところです。
ここでややこしくなるのが、アナログスティックの↑を押した時にキャラクターの前方方向を押した事に出来た場合でも
アナログスティックの↑を押した時はワールド軸の前方からの角度を求める
ということです。
敵の角度もワールド軸の前方からの角度で、アナログスティックの角度もワールド軸の前方からの角度を求めます。
なのでキャラクターを90度回転させた時にアナログスティックの↑を押した時に90度が得られるようにします。
入力はキャラクターの前方から見た方向を得る必要はあるが、比較する敵の角度とアナログスティックの入力方向はワールド軸の前方からの角度で計算するということですね。
わたくしはここら辺でかなりこんがらがりました。(^_^;)
一番近い敵がわかったらアナログスティックの入力角度の差によって砂嵐の濃さと音量を変化させます。
またメインカメラのCameraとAudio Listenerコンポーネントを無効にし、一番近い敵に設定されているCameraとAudio Listenerを有効にします。
これが視界ジャック機能の概要となります。
視界ジャック機能の作成
視界ジャック機能をどうやって作るか?がわかったので、実際に作っていきましょう。
操作キャラクターの作成
まずは操作キャラクターを作成します。
今回の操作キャラクターは↑のキーを押したらキャラクターの前方に進み、↓を押したら後方に進み、←→を押したらその方向に回転するようなキャラクターになります。
キャラクターが前に進むには必ず↑のキーを押す必要があり、←→のキーで方向を定めます。
キャラクターのモデルにはスタンダードアセットのEthanを使用し、CharacterControllerの取り付けをしてコライダの調整をしておきます。
キャラクターのAnimatorControllerは立っている状態のIdle状態、歩いているWalk状態、後ろ向きに歩くWalkBack状態を作成します。
それぞれの状態のアニメーションは以下のように設定します。
IdleにはスタンダードアセットのHumanoidIdle
WalkにはスタンダードアセットのHumanoidWalk
WalkBackにはスタンダードアセットのHumanoidWalk
を設定します。
またWalkBack状態を選択し、Speedに-1を設定してWalkBackでは歩くアニメーションを逆再生するようにします。
アニメーションパラメータ―にFloat型のSpeedを作成します。
Idle→Walkの条件はSpeedがGreaterで0.1
Walk→Idleの条件はSpeedがLessで0.1
Idle→WalkBackの条件はSpeedがLessで-0.1
WalkBack→Idleの条件はSpeedがGreaterで-0.1
でそれぞれHas Exit Timeのチェックを外して直ぐに次の状態に遷移するようにします。
新しいスクリプトViewJackCharaスクリプトを作成し、キャラクターであるEthanに取り付けます。
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class ViewJackChara : MonoBehaviour { public enum HeroState { Normal, ViewJack } private CharacterController characterController; private Animator animator; private Vector3 velocity; private HeroState heroState = HeroState.Normal; [SerializeField] private float rotateSpeedOfChara = 45f; [SerializeField] private float walkSpeedOfChara = 1.25f; // Start is called before the first frame update void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); heroState = HeroState.Normal; } // Update is called once per frame void Update() { if(characterController.isGrounded && heroState == HeroState.Normal ) { velocity = Vector3.zero; var hInput = Input.GetAxis("Horizontal"); if(hInput != 0f) { animator.SetFloat("Speed", 1f); transform.Rotate(new Vector3(0f, hInput * rotateSpeedOfChara * Time.deltaTime, 0f)); } else { animator.SetFloat("Speed", 0f); } var vInput = Input.GetAxis("Vertical"); if(vInput != 0f) { animator.SetFloat("Speed", vInput); velocity += transform.forward * vInput * walkSpeedOfChara; } else if(Mathf.Approximately(hInput, 0f)) { animator.SetFloat("Speed", 0f); } } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } public void SetState(HeroState state) { heroState = state; if(heroState == HeroState.ViewJack) { velocity = new Vector3(0f, velocity.y, 0f); animator.SetFloat("Speed", 0f); } } public HeroState GetState() { return heroState; } } |
スクリプトで行っていることは概要で説明したキャラクターの動きをさせる事とキャラクターの状態の取得と設定を出来るメソッドを用意していることです。
横軸の入力があればtransform.Rotateを使ってキャラクターを回転させます。
1 2 3 4 5 6 7 8 9 10 | var hInput = Input.GetAxis("Horizontal"); if(hInput != 0f) { animator.SetFloat("Speed", 1f); transform.Rotate(new Vector3(0f, hInput * rotateSpeedOfChara * Time.deltaTime, 0f)); } else { animator.SetFloat("Speed", 0f); } |
横に回転する時はアニメーションパラメータ―のSpeedを1にしてWalk状態に遷移させます。
縦軸の入力の場合はアニメーションパラメータ―のSpeedに縦軸の入力値をそのまま入れて↑だったらWalk状態、↓だったらWalkBack状態へと遷移するようにします。
1 2 3 4 5 6 7 8 9 10 | var vInput = Input.GetAxis("Vertical"); if(vInput != 0f) { animator.SetFloat("Speed", vInput); velocity += transform.forward * vInput * walkSpeedOfChara; } else if(Mathf.Approximately(hInput, 0f)) { animator.SetFloat("Speed", 0f); } |
また進む方向はキャラクターの向いている方向の前後になります。
キャラクターを移動出来るのはキャラクターの状態がHeroState.Normalの時だけで、HeroState.ViewJackの時は移動出来ません。
これで操作キャラクターが出来ました。
視界ジャック機能のスクリプトの作成
視界ジャック機能のスクリプトを作成していきます。
新しくViewJackScriptという名前のスクリプトを作成しEthanに取り付けます。
スクリプトが長いので少しづつ解説します。
フィールド宣言部
まずはフィールド宣言部を見ていきます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ViewJackScript : MonoBehaviour { // 全部の敵のリスト [SerializeField] private List<Transform> enemyList = new List<Transform>(); // 視界ジャックをしているかどうか private bool isViewJack; // 視界ジャックをする事が出来る主人公の視野角度 [SerializeField] private float fieldOfView = 135f; // 視界ジャックする押した方向と敵との制限角度 [SerializeField] private float limitAngleOfViewJack = 25f; // スノーノイズの最大の濃さの時の角度 [SerializeField] private float snowNoiseMaxValueAngleLimit = 40f; // スノーノイズの濃さのオフセット値 [SerializeField] private float snowNoiseIntensityOffset = 15f; [SerializeField] private ViewJackChara viewJackChara; [SerializeField] private GameObject snowNoiseImageObj; // メインカメラ [SerializeField] private Camera mainCamera; // メインカメラのAudioListener [SerializeField] private AudioListener mainAudioListener; // 視界ジャック中のカメラ private Camera currentCamera; // 視界ジャック中のカメラのAudioListener private AudioListener currentAudioListener; } |
enemyListには視界ジャックが出来る敵をインスペクタで設定します。
isViewJackは視界ジャックをしているかどうかのフラグです。
fieldOfViewは視界ジャックをする敵のキャラクターの視野の角度です。この範囲外の敵は視界ジャック適用外とします。
limitAngleOfViewJackはアナログスティックを押した角度と敵の角度の差がこの範囲内の時に一番近い敵かどうかの判定をします。
snowNoiseMaxValueAngleLimitは砂嵐の強度の最大値を設定します。
snowNoiseIntensityOffsetはアナログスティックの角度と敵の角度が完全に一致した時の砂嵐の強度の補正値を設定します。
viewJackCharaはキャラクターの操作スクリプトであるViewJackCharaです。
snowNoiseImageObjは砂嵐を表示するRawImageゲームオブジェクトを設定します。
mainCameraはメインカメラを設定します。
mainAudioListenerはメインカメラのAudio Listenerを設定します。
currentCameraは視界ジャックをしている敵のカメラを入れます。
currentAudioListenerは視界ジャックをしている敵のカメラに設定されているAudioListenerを入れます。
Updateメソッド
次にUpdateメソッドを見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // Update is called once per frame void Update() { if (Input.GetButtonDown("ViewJack")) { isViewJack = !isViewJack; if (viewJackChara.GetState() == ViewJackChara.HeroState.Normal) { viewJackChara.SetState(ViewJackChara.HeroState.ViewJack); snowNoiseImageObj.GetComponent<RawImage>().enabled = true; snowNoiseImageObj.GetComponent<AudioSource>().Play(); } else { snowNoiseImageObj.GetComponent<RawImage>().enabled = false; snowNoiseImageObj.GetComponent<AudioSource>().Pause(); InitializeViewJack(); viewJackChara.SetState(ViewJackChara.HeroState.Normal); } } // 視界ジャック状態 if (isViewJack) { ViewJack(); } } |
Input Managerで設定したViewJackボタンを押した時に視界ジャック機能のオン・オフをします。
Input Managerに関しては下記の記事の「UnityのInputManagerでキーとボタンの設定をする」の項目を参照して設定してみてください。
PS4コントローラーの設定ではMotion JoyやBetter PS3はいらないっぽい?ので他のソフトのダウンロードやインストールはせず「UnityのInputManagerでキーとボタンの設定をする」の項目だけまずは設定してみてください。
(今確認したらPS3コントローラーもMotion JoyやBetter PS3がいらなくなってるかも?)

わたくしの環境ではPS4コントローラーの△ボタンはjoystick button 3に割り当てました。
キャラクターの状態をGetStateメソッドで取得し、ノーマル状態なら視界ジャック状態へ、視界ジャック状態ならノーマル状態へと切り替えます。
その時に砂嵐を表示しているRawImageの有効・無効、砂嵐の音声を設定しているAudioSourceの再生・停止を切り替えます。
視界ジャック状態からノーマル状態へと移行する時はInitializeViewJackメソッドを呼び出して視界ジャック設定の初期化を行います。
isViewJackがtrueの時は視界ジャック中なのでViewJackメソッドを呼び出します。
ViewJackメソッド
ViewJackメソッドを見ていきます。
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | void ViewJack() { // 方向キーを押している時だけ計算 if (Input.GetAxis("Horizontal") != 0f || Input.GetAxis("Vertical") != 0f) { // カメラと音量をリセット InitializeViewJack(); // 入力方向をXZ軸に割り当てる var inputDirection = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); // 一番近い敵 Transform closestEnemy = null; // 押した方向と一番近い敵の角度 var closestEnemyRot = Mathf.Infinity; // 全部の敵から押した方向と一番近い敵を探す foreach (var enemy in enemyList) { // XZ平面での主人公から見た敵の方向ベクトルの計算 var enemyDirection = new Vector3(enemy.position.x, 0f, enemy.position.z) - new Vector3(transform.position.x, 0f, transform.position.z); // 主人公前方からの敵の角度(0~180度に収める) var enemyAngleFromFrontOfChara = Vector3.Angle(enemyDirection, transform.forward); // 主人公前方からの敵の角度を内積で計算する場合 //enemyAngleFromFrontOfChara = Vector3.Dot(transform.forward.normalized, enemyDirection.normalized); // ワールド軸の前方から見た敵の角度(0~360度) var enemyRot = Quaternion.FromToRotation(Vector3.forward, enemyDirection).eulerAngles.y; // キーを押した方向を主人公の向きに合わせて回転させた角度 var inputDirectionRot = Mathf.Repeat(Quaternion.LookRotation(inputDirection).eulerAngles.y + transform.eulerAngles.y, 360f); // 主人公と敵の角度の差を計算 var heroAndEnemyAngle = Mathf.Round(Mathf.Abs(enemyRot - inputDirectionRot)); // 主人公の視界ジャックの範囲内に敵がいる場合だけ if (0f <= enemyAngleFromFrontOfChara && enemyAngleFromFrontOfChara <= fieldOfView) { // 0度から反転して360へと移行した時の補正 if (heroAndEnemyAngle > limitAngleOfViewJack) { heroAndEnemyAngle = 360f - heroAndEnemyAngle; } // 主人公の視界ジャック範囲内の一番近い敵を登録 if (0f <= heroAndEnemyAngle && heroAndEnemyAngle <= limitAngleOfViewJack) { if (closestEnemy == null) { closestEnemy = enemy; closestEnemyRot = heroAndEnemyAngle; } else if (heroAndEnemyAngle < closestEnemyRot) { closestEnemy = enemy; closestEnemyRot = heroAndEnemyAngle; } } } } // 一番近い敵と入力方向の角度の差によって砂嵐の透明度と音量を変化させる if (closestEnemy != null) { Debug.Log("一番近い敵:" + closestEnemy.name + closestEnemyRot); snowNoiseImageObj.GetComponent<RawImage>().color = new Color(1f, 1f, 1f, Mathf.InverseLerp(0f, snowNoiseMaxValueAngleLimit, closestEnemyRot + snowNoiseIntensityOffset)); snowNoiseImageObj.GetComponent<AudioSource>().volume = Mathf.InverseLerp(0f, snowNoiseMaxValueAngleLimit, closestEnemyRot + snowNoiseIntensityOffset); // 敵の視界をジャックする currentCamera = closestEnemy.GetComponentInChildren<Camera>(); currentCamera.enabled = true; currentAudioListener = closestEnemy.GetComponentInChildren<AudioListener>(); currentAudioListener.enabled = true; mainCamera.enabled = false; mainAudioListener.enabled = false; } // キーを押していない時でメインカメラが有効でない時は初期化 } else if (!mainCamera.enabled) { InitializeViewJack(); } } |
方向キーの上下左右を押している時はInitializeViewJackメソッドを呼んで初期化します。
1 2 3 4 5 6 7 8 | // 入力方向をXZ軸に割り当てる var inputDirection = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); // 一番近い敵 Transform closestEnemy = null; // 押した方向と一番近い敵の角度 var closestEnemyRot = Mathf.Infinity; |
アナログスティックの入力をXZ軸に割り当てます。
closestEnemyはアナログスティックの入力方向と一番近い敵を入れます。
closestEnemyRotは一番近い敵とアナログスティックの角度の差を入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 全部の敵から押した方向と一番近い敵を探す foreach (var enemy in enemyList) { // XZ平面での主人公から見た敵の方向ベクトルの計算 var enemyDirection = new Vector3(enemy.position.x, 0f, enemy.position.z) - new Vector3(transform.position.x, 0f, transform.position.z); // 主人公前方からの敵の角度(0~180度に収める) var enemyAngleFromFrontOfChara = Vector3.Angle(enemyDirection, transform.forward); // 主人公前方からの敵の角度を内積で計算する場合 //enemyAngleFromFrontOfChara = Vector3.Dot(transform.forward.normalized, enemyDirection.normalized); // ワールド軸の前方から見た敵の角度(0~360度) var enemyRot = Quaternion.FromToRotation(Vector3.forward, enemyDirection).eulerAngles.y; // キーを押した方向を主人公の向きに合わせて回転させた角度 var inputDirectionRot = Mathf.Repeat(Quaternion.LookRotation(inputDirection).eulerAngles.y + transform.eulerAngles.y, 360f); // 主人公と敵の角度の差を計算 var heroAndEnemyAngle = Mathf.Round(Mathf.Abs(enemyRot - inputDirectionRot)); |
enemyDirectionはY軸を0に固定したキャラクターから見た敵の方向ベクトルを計算しています。
enemyAngleFromFrontOfCharaは敵の方向とキャラクターの方向の角度を求めてキャラクターの視野内にいるかどうかの判定に使います。
実際の角度とは別に二つの正規化されたベクトルの内積を求めると0はキャラクターの横、1は前方、-1は後方になるのでそれを使って視野内にいるかどうかも求められますが、今回はわかりやすく角度を使いました。
enemyRotはワールド軸の前方からの敵のY軸の角度を求めています。
inputDirectionRotはアナログスティックの入力方向のY軸の角度にキャラクター自身の向いているY軸の角度を足してワールド軸の前方からのアナログスティックの角度を計算しています。
キャラクター自身の角度を足さないとアナログスティックの入力方向はキャラクターの向きにかかわらず全て同じになってしまいます。
Mathf.Repeatを使って0~360度に値を補正しています(角度を0~360度未満にする為)。
heroAndEnemyAngleではワールド軸前方からの敵の角度とワールド軸前方からのアナログスティックの角度を引いてMathf.Absで絶対値を計算し、Mathf.Roundで指定した値に最も近い整数を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 主人公の視界ジャックの範囲内に敵がいる場合だけ if (0f <= enemyAngleFromFrontOfChara && enemyAngleFromFrontOfChara <= fieldOfView) { // 0度から反転して360へと移行した時の補正 if (heroAndEnemyAngle > limitAngleOfViewJack) { heroAndEnemyAngle = 360f - heroAndEnemyAngle; } // 主人公の視界ジャック範囲内の一番近い敵を登録 if (0f <= heroAndEnemyAngle && heroAndEnemyAngle <= limitAngleOfViewJack) { if (closestEnemy == null) { closestEnemy = enemy; closestEnemyRot = heroAndEnemyAngle; } else if (heroAndEnemyAngle < closestEnemyRot) { closestEnemy = enemy; closestEnemyRot = heroAndEnemyAngle; } } } |
敵がキャラクターの視野角度内にいる敵だけを判定します。
敵と入力の角度が視界ジャック範囲より大きい時はheroAndEnemyAngleを360から引いて再度代入します。
これは角度が0から反時計回りに回転した時に359→358と0から数字が増えてしまうので、そういった時の為に360から引いた値にします。
例えば、敵の角度が355で入力の角度が0の時は実際はheroAndEnemyAngleが5だが、計算上355になってしまうのでこれの対応です。
その後、敵と入力の角度の差がlimitAngleOfViewJack(視界ジャックをする範囲)内の時でclosestEnemy(一番近い敵)が設定されていない時はその敵を登録し、敵と入力の角度の差をclosestEnemyRotに入れます。
既にclosestEnemyが登録されている時は今調べている敵と入力の角度差とclosestEnemyRotの大きさを調べで角度差が小さい方をclosestEnemyとclosestEnemyRotに設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 一番近い敵と入力方向の角度の差によって砂嵐の透明度と音量を変化させる if (closestEnemy != null) { Debug.Log("一番近い敵:" + closestEnemy.name + closestEnemyRot); snowNoiseImageObj.GetComponent<RawImage>().color = new Color(1f, 1f, 1f, Mathf.InverseLerp(0f, snowNoiseMaxValueAngleLimit, closestEnemyRot + snowNoiseIntensityOffset)); snowNoiseImageObj.GetComponent<AudioSource>().volume = Mathf.InverseLerp(0f, snowNoiseMaxValueAngleLimit, closestEnemyRot + snowNoiseIntensityOffset); // 敵の視界をジャックする currentCamera = closestEnemy.GetComponentInChildren<Camera>(); currentCamera.enabled = true; currentAudioListener = closestEnemy.GetComponentInChildren<AudioListener>(); currentAudioListener.enabled = true; mainCamera.enabled = false; mainAudioListener.enabled = false; } // キーを押していない時でメインカメラが有効でない時は初期化 } else if (!mainCamera.enabled) { InitializeViewJack(); } |
一番近い敵が存在した場合は砂嵐動画を再生するRawImageの透明度とAudioSourceの音量を調整します。
colorやvolumeは0~1の間の数値を指定する必要があるので砂嵐の透明度と音量は最大値を1、最低値を0に変換する必要があります。
今回の場合は最大値はsnowNoiseMaxValueAngleLimitで指定した値にしていて、初期値を40にしているので40の時に1にして0の時の0にする必要があります。
なので、Mathf.InverseLerpを使って比率を求めます。
Mathf.InverseLerpは第1引数と第2引数の間の第3引数の割合を求めます。
例えば
1 2 3 | Mathf.InverseLerp(0f, 100f, 60f); |
上の場合は結果として0.6が得られます。
closestEnemyの子要素のCameraコンポーネントやAudioListenerコンポーネントを取得し有効にします。
逆にMainCameraのCameraとAudioListenerは無効にします。
方向キーを押していない時でメインカメラが無効の状態の時はInitializeViewJackメソッドを呼び出し設定の初期化をします。
InitializeViewJackメソッド
InitializeViewJackメソッドを見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 視界ジャックの初期化処理 void InitializeViewJack() { mainCamera.enabled = true; mainAudioListener.enabled = true; // 視界ジャック中のカメラがあったら無効にする if (currentCamera != null) { currentCamera.enabled = false; currentCamera = null; } if (currentAudioListener != null) { currentAudioListener.enabled = false; currentAudioListener = null; } snowNoiseImageObj.GetComponent<RawImage>().color = new Color(1f, 1f, 1f, 1f); snowNoiseImageObj.GetComponent<AudioSource>().volume = 1f; } |
InitializeViewJackメソッドではメインカメラのCamera、AudioListenerコンポーネントを有効にし、currentCameraとcurrentAudioListenerが設定されていれば無効にしnullを設定します。
また砂嵐の設定も初期値に戻します。
これで視界ジャックのスクリプトが出来ました。
インスペクタでいくつか設定する必要がありますが、その前に砂嵐やメインカメラの設定、敵キャラクターの作成をしていきます。
砂嵐の設定
視界ジャック時に表示する砂嵐を作成していきます。
表示する砂嵐動画は

の記事で作成したものを使います。
上の記事では動画内に音声も含ませていますが、Unity内で連続再生させると音声がループする時に途切れてしまう為、音声は別途Audacityで3秒程のザーッという音声をWAV形式で出力しUnityに取り込んでおきます。
取り込んだ動画のインスペクタは
取り込んだ音声のインスペクタは
のように設定しました。
次にヒエラルキー上で右クリック→UI→RawImageを選択します。
RawImageの横と縦の幅はstretchにして画面全体に流れるようにします。
作成したRawImageには先ほど取り込んだ砂嵐の動画を流しますが、動画を流すにはVideo Playerコンポーネントを使用する必要があります。
Video PlayerではRender Modeの設定で動画の出力先を設定出来ます。
今回は一旦Render Textureに動画を出力し、このRender TextureをRawImageのテクスチャに設定することで動画を流すようにします。
Assetsフォルダ内で右クリック→Create→Render Textureを選択し名前をViewJackとします。
ViewJackを選択しインスペクタでSizeを512に変更します。
レンダーテクスチャが出来たので設定をしていきます。
RawImageゲームオブジェクトを選択し、インスペクタのAdd Componentボタンを押してVideo→Video Playerを選択します。
RawImageゲームオブジェクトのRaw Imageコンポーネントの横のチェックを外します。
これで砂嵐動画は最初は表示されません。
Raw ImageのTextureには先ほど作成したViewJackレンダーテクスチャを設定します。
こうすることでVideo PlayerでViewJackレンダーテクスチャに出力し、それをRaw ImageのTextureに設定する事で動画を流します。
Video PlayerのVideo Clipには砂嵐の動画を設定し、Play On Awakeにチェックを入れて自動で再生され、Loopにチェックを入れて繰り返し再生されるようにします。
Render ModeではRender Textureにしレンダーテクスチャに出力されるようにし、Target Textureでは実際のレンダーテクスチャファイルを指定します。
Audio OutputはNoneにして音声は出力しないようにします。
次にRawImageのインスペクタのAdd ComponentからAudio→Audio Sourceを選択します。
AudioClipに砂嵐用の音声クリップを設定し、Loopにチェックを入れて繰り返し再生されるようにします。
AudioClipのLoopにチェックした場合は繰り返し再生しても終わりと初めの部分で途切れるという事はありませんでした。
これで砂嵐の設定が終わりました。
メインカメラの設定
メインカメラの設定をしていきます。
メインカメラはシーン作成時にあるMainCameraをそのまま使用します。
インスペクタのAdd ComponentからScripts→UnityStandardAssets.Utility→SmoothFollowを選択します。
Targetには操作キャラクターの頭付近に空のゲームオブジェクトを作成し、名前をCameraFocusPositionとし、それをTargetに設定しました。
この辺りは

を参照してください。
敵キャラクターの設定
敵キャラクターの配置と設定をします。
今回はアセットストアで無料でダウンロード出来るゾンビを敵キャラクターとして使用します。
アセットストアで検索窓に「zombi」と入れ、Sort byでPrice(Low to High)を選択すると上位で出てきます。
インポートしたらZombie→Prefabs→Zombiをヒエラルキー上に配置します。
ヒエラルキー上のZombiを選択した状態で右クリック→Cameraを選択します。
CameraをZombiの目の辺りに移動します(ゾンビのボーンのBip01 Headの子要素に移動させ目の辺りに移動)。
Cameraを選択しインスペクタのCameraコンポーネントとAudio Listenerコンポーネントのチェックを外し無効化しておきます。
今回は敵キャラクターは動かさないのでカメラは単にZombiの子要素に置きました。
またあらかじめAnimatorに設定されているアニメーターコントローラーは選択してDeleteキーを押し未設定の状態にしておきます。
これで敵キャラクターの設定は終了です。
視界ジャック機能スクリプトViewJackScriptのインスペクタの設定
これで機能が全て出来たのであとはViewJackScriptのインスペクタの設定をするだけです。
EnemyListには全ての敵キャラクターを登録します。
ViewJackCharaには自身
SnowNoiseImageObjにはRawImage
MainCameraにはMainCamera
MainAudioListenerにはMainCamera
をドラッグ&ドロップして設定します。
これで視界ジャック機能の完成です。(^^)/
終わりに
今回の機能で方向キーと敵の方向を比較する時の角度の計算で結構混乱しました。
角度計算するにも色々なメソッドがあって、Vector3.Angle、Vector3.SignedAngle、Quaternion.Angle、Quaternion.LookRotation、Quaternion.FromToRotation等々。

どれが一番使えるかなぁと色々試しているうちに何が何やらわからなくなりはまってしまいました。
もっと賢いやり方もあるかもしれませんが、とりあえず出来た方法で作りました。
SIRENの視界ジャックがどのように作られているかはわかりませんが、似たようなものは出来たのではないかと思います。
さらに機能を近づけるならば、一番近い敵の視界ジャックをしている時に〇ボタンを押したらその敵の視界ジャックに固定するという風にすると良さそうですね。
視界ジャック機能を作っているうちにまたSIRENやりたくなってきたなぁ・・・・(´Д`)
新作をPS4で出してくれないかなぁ・・・・(´Д`)
最近市販のホラーゲーム少なくないですか?(´Д`)
(´-ε-`)ボソボソ