ロッカーを左クリックすると開いて、FPSコントローラーで動かすプレイヤーが中に入ります。もう一度左クリックするとロッカーから出ます。
まず一人称のカメラからレイを飛ばして手が届く位置にロッカーがあるかどうかを調べます。
// FirstPersonController
[SerializeField] float armLength = 3f; // 腕の長さ
[SerializeField] GameObject hand; // 手の画像
Animator hitAnimator; // ヒットしたオブジェクトのAnimatorコンポーネント
// ---
// Update()
// ワープ中でない時
if (!warp)
{
// 何もしていないとき
if (state == 0)
{
// 顔からレイを投げる
if (Physics.Raycast(m_Camera.transform.position, m_Camera.transform.forward, out hit, 50f, ~(1 << 9)))
{
hitCollider = hit.collider;
hitObj = hitCollider.gameObject;
hitTag = hitCollider.tag;
float hitDistance = hit.distance;
if (armLength >= hitDistance) // 届く位置にあるとき
{
if(hitTag == "Ladder") // 梯子のとき
{
// ...
}
else if (hitTag == "Closet") // ロッカーやクローゼット等のとき
{
hand.SetActive(true); // 手のマークを出す
if (Input.GetMouseButtonDown(0)) // マウスの左クリック
{
if (hit.transform.parent) // 扉のとき
{
hitTransform = hit.transform.parent;
}
else { // 扉でないとき
hitTransform = hit.transform;
}
// クローゼット開く
hitAnimator = hitTransform.GetComponent<Animator>();
hitAnimator.SetBool("Open", true); // 扉を開くアニメーション
hitObj = hitTransform.gameObject;
AudioManager.PlayClosetSound(hitObj, 0); // 扉が開く音
state = 4; // アニメーション状態にする
if (Warp(hitTransform, false)) // プレイヤーを扉の前までワープさせる
{
hand.SetActive(false); // 手のマークを消す
}
else // ワープできないとき
{
state = 0;
}
}
}
}
}
}
// 状態1
else if(state == 1)
{
// ...
}
// 状態2
else if(state == 2)
{
// ...
}
// ...
}
ヒットしたオブジェクトのアニメーターを使って扉を開くアニメーションを開始します。扉はロッカーの子オブジェクトなので、ヒットしたオブジェクトに親があれば親オブジェクトを使います。また、音声を管理するスクリプトで扉が開く音を再生します。
プレイヤーの状態が整数値で管理されています。クリックしたあとは別の状態に切り替えてレイを飛ばさないようにしています。
扉の開閉アニメーションはまず開くアニメーションを作って、閉じるときはアニメーターコントローラーでそれを逆再生します。
その後、プレイヤーが毎回同じ位置に来てロッカーの方をまっすぐ向いてほしいので、はしごを登るで使った、キャラクターコントローラーをオフにしてtransformでプレイヤーを動かすスクリプトでプレイヤーをワープさせます。
public bool Warp(Transform t,bool pri)
{
m_MouseLook.prioritizeRotate = pri; // 回転値を優先
// 梯子のとき
if (state == 2)
{
// ...
}
// クローゼットに入るとき
else if (hitTag == "Closet" && state != 3)
{
m_CharacterController.enabled = false; // キャラクターコントローラーをオフ
warp = true; // ワープをオン
warpDestination = t.position + t.forward * 2f; // ワープする位置
warpDestination.y = transform.position.y; // 高さを揃える
// ロッカーと向かい合うようにカメラを回転させる
m_MouseLook.Warp(t.rotation * Quaternion.Euler(new Vector3(0, 180f, 0)));
return true;
}
// クローゼットから出る時
else if (state == 3)
{
m_CharacterController.enabled = false; // キャラクターコントローラーをオフ
warp = true; // ワープをオン
warpDestination = t.position; // ロッカーと同じ位置
warpDestination.y = transform.position.y; // 高さを揃える
// ロッカーの正面と同じ方向にカメラを回転
m_MouseLook.Warp(t.rotation);
return true;
}
else
{
return false;
}
}
public void Warp(Vector3 warpPos, Quaternion warpRot, bool pri)
{
m_MouseLook.prioritizeRotate = pri;
m_CharacterController.enabled = false; // キャラクターコントローラーをオフ
warp = true; // ワープを開始
warpDestination = warpPos; // ワープする位置
m_MouseLook.Warp(warpRot); // ワープする回転値
}
引数にはロッカーや梯子などのトランスフォームと、回転を優先するかどうかのbool値を渡します。このトランスフォームを使って、ワープする目的地の座標と回転値を計算します。
計算方法ははしごに乗るときやロッカーに入る前、ロッカーから出るときで違うので、プレイヤーの状態やヒットしたタグ名などを使って場合分けしています。そのせいでわかりにくくなってしまったので、目的地の位置と回転を渡すバージョンも作りました。
第2引数がfalseだと、目的地に十分近づいたらワープが終了します。目的地は近くにあるけどかなりカメラを回転する必要があるときはtrueにします。このためにMouselookクラスにも手を加えます。
float WarpTargetAngleAxisY; // 目的の回転値
public bool prioritizeRotate;
public bool warp = false;
// LookRotation
if (!warp)
{
// ...
}
else {
if (prioritizeRotate && Mathf.Abs(WarpTargetAngleAxisY - character.localRotation.eulerAngles.y) <= 30f)
{
smooth = false;
prioritizeRotate = false;
}
}
m_MouseLook.Warp()では、目的の回転値を設定し、スムーズに切り替えて、マウスの入力を受け付けないようにするだけです。
ワープ中はFirstPersonControllerのUpdateでtransformに値を入れて少しずつプレイヤーの位置を目的地に近づけます。十分近づくと最後に目的地へ瞬時に移動させたあとにワープ終了の関数を呼びます。
// Update()
// ワープ中
if(warp)
{
// 今の位置からワープ位置まで
Vector3 warpDir = warpDestination - transform.position;
// ワープ位置に十分近づいたら
if (!m_MouseLook.prioritizeRotate && warpDir.magnitude <= 0.1f)
{
// ワープ位置へ一気に移動
transform.position = warpDestination;
StartCoroutine("EndWarp");
}
else
{
// 少しずつワープ位置へ移動
transform.position += (warpDir * Time.deltaTime * warpSpeed);
}
}
回転を優先するときは十分近づいてもワープが終了しないようにしています。
ワープが終了すると、Mouselookのスムーズを切って1フレーム待つことで、カメラが最後に瞬時に目的の回転値になるようにしています。
IEnumerator EndWarp()
{
if (warp)
{
endWarpIsPlaying = true;
m_MouseLook.smooth = false; // 先にスムーズを切る
warp = false; // 移動できるようにする
yield return null;
if (state == 2)
{
// ...
}
else if (hitTag == "Closet" && state != 3)
{
hitAnimator.SetBool("Open", false); // ロッカーを閉じる
PlayMotion(true, 3, 0); // プレイヤーをアニメーションさせる
}
else if (state == 3)
{
hitAnimator.SetBool("Open", false);
PlayMotion(true, 0, 1); // プレイヤーをアニメーションさせる
}
// バケツにのるとき
else if (hitTag == "Bucket" && state == 5)
{
// ...
}
else if (state == 5)
{
// ...
}
else if (state == 7)
{
// ...
}
m_MouseLook.EndWarp();
endWarpIsPlaying = false;
}
}
この後にFPSコントローラーの移動方法を変えてはしごに沿って移動したいときもあれば、続けてアニメーションさせたいときもあるので、ここでも場合分けしています。
m_MouseLook.EndWarp()ではワープをやめて、スムーズの値を元に戻します。
FPSプレイヤーをアニメーションさせるには、プレイヤーを空のゲームオブジェクトの子にして、空のゲームオブジェクトをアニメーションさせる方法をとっています。
// アニメーションで動かす
public void PlayMotion(bool b, int s = -1, int i = -1)
{
if (b)
{
LockCamera = true; // カメラをロック
canMove = false; // 移動できないようにする
m_CharacterController.enabled = false; // キャラクターコントローラーをオフ
vehicle.position = transform.position; // 位置を合わせる
vehicle.rotation = transform.rotation; // 回転を合わせる
transform.parent = vehicle; // プレイヤーをビークルの子にする
m_MouseLook.ResetCharacterRot(); // 回転をリセット
state = 4;
if (i == 0) vehicleAnimator.SetTrigger("Closet");
else if (i == 1)
{
vehicleAnimator.SetTrigger("ClosetOut");
}
else if (i == 2)
{
vehicleAnimator.SetTrigger("BucketIn");
}
else if (i == 3)
{
vehicleAnimator.SetTrigger("BucketOut");
}
else
{
if (s != -1) state = s;
}
}
else
{
LockCamera = false; // カメラのロックを解除
m_CharacterController.enabled = true; // キャラクターコントローラーをオン
Quaternion rot = transform.rotation; // 回転値を保存
transform.parent = null; // 親子関係を解消
m_MouseLook.SetQuaternion(rot); // 回転値を設定
// バケツに乗った時
if (hitTag == "Bucket")
{
// ...
}
else
{
canMove = true; // 動けるようにする
if (s != -1) state = s;
}
}
}
第一引数がtrueの時アニメーションが始まって、アニメーションが終わるとステートマシンビヘイビアから、falseにして再度呼ばれます。第2引数にはこの後に変えたい状態を渡すようにしてみました。
ここでも、セットするパラメーターやその後にやりたいことが違うので場合分けしています。そのために第三引数を使っています。
ロッカーを出入りする流れ
ロッカーをクリックすると、まずロッカーの開くアニメーションを再生しながら、プレイヤーをロッカーの正面でロッカーの方をまっすぐ向く状態へワープさせ、ワープが終わるとロッカーを閉めながら、プレイヤーをアニメーションさせてロッカーの中に入ります。
このときに、プレイヤーがロッカーをまっすぐ向いていないとロッカーの外に出ることがあるので、念の為、アニメーションの終了時にロッカーの中心へワープさせても良いかもしれません。
ロッカーの中にいるときはプレイヤーの状態が変わっていてレイを飛ばさす、その状態のときに左クリックすると、ロッカーを開きながら、ロッカーの真ん中でロッカーの出口の方を向いた状態へワープさせます。ワープが終わると、ロッカーを閉めるアニメーションと外に出るアニメーションが始まります。外に出ると状態が0に戻ります。
ロッカーを閉める音は、プレイヤーを乗せるオブジェクトのアニメーターコントローラーで、プレイヤーがロッカーの中に入ったり出たりするモーションのアタッチされたステートを出るときに、ステートマシンビヘイビアからメソッドを呼んで再生しています。
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (stateInfo.IsName("VehicleAnimation_Closet"))
{
sc_fps.PlayMotion(false, 3);
AudioManager.PlayClosetSound(sc_fps.hitObj, 1);
}
//...
これでFPSコントローラーでロッカーに出たり入ったりできるようになりました。