丸い爪でも立ててみる

Unity開発の事一覧

回 スクリプトでアイテムを画面中央で回転させて調べるアレを実装【Unity】

はじめまして、すらです。

今回はアイテムを画面中央で回転させて調べる、ゲームでよくあるシステムを実装できたのでその手順を残していきたいと思います。

実際に完成したものがこちらです。

私はURP環境なので、Build in やHDRPとは実装の違いはあると思いますので注意。

環境

Unity : 2022.3.14f1

project : URP

まずはやりたいことをテキスト化

今回やりたいことを順番にしてみます。

 

①プレイヤーの視点が調べられるアイテムに向いているか

②調べられるアイテムである場合は、なんらかのキー入力でアイテムを画面中央へ移動させる

③アイテム注目時に、背景にブラー(ぼかし)をかけ、アイテムを光で照らして強調させる

④アイテムを回転させられるようにする

⑤なんらかのキー入力でアイテム注目状態を解除させる

 

おおまかに分けるとこんな感じでしょうか。

①から順番に解説していきます。

 

 

前準備

①から説明すると言ったな、あれは嘘だ。

まずはプレイヤーの状態を管理するスクリプトを作成しましょう。

プレイヤーの状態とは例えば、「歩いている」「走っている」「ジャンプしている」などのことですね。

もちろんゲームによっては毒や麻痺など、状態異常中のものもあるでしょう。

そのような状態を管理しておくことで、〇〇の時は△△の処理をさせるといった条件付けをさせやすく、特定の状態時に特定の処理をさせるといった実装が非常に楽になります。

そこでPlayerStateControllerを作成しました。

using UnityEngine;


public class PlayerStateController : MonoBehaviour
{
    public enum PlayerState{
        Idle, //止まっている時
        Walk, //歩いている時
        Run, //走っている時
        Item //アイテム確認画面の時
    }

    public PlayerState currentState = PlayerState.Idle;


    // Start is called before the first frame update
    void Start()
    {
        switchState(PlayerState.Idle);
    }


    // Update is called once per frame
    void Update()
    {
        if(currentState != PlayerState.Item)
        {
            if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.W) ||
            Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.A) ||
            Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.S) ||
            Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.D))
            {
                switchState(PlayerState.Run);
            }
            else if (!Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.W) ||
            !Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.A) ||
            !Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.S) ||
            !Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.D))
            {
                switchState(PlayerState.Walk);
            }
            else
            {
                switchState(PlayerState.Idle);
            }


        }
    }


    public void switchState(PlayerState newState)
    {
        // 状態切り替え時の処理を追加
        OnExitState(currentState);
        currentState = newState;
        OnEnterState(currentState);
    }

    void OnEnterState(PlayerState state)
    {
        // 各状態に入ったときの処理を実行
        switch (state)
        {
            case PlayerState.Idle:

                break;
            case PlayerState.Walk:

                break;
            case PlayerState.Run:

                break;
            case PlayerState.Item:

                break;
        }
    }


    void OnExitState(PlayerState state)
    {
        // 各状態から出たときの処理を実行
        switch (state)
        {
            case PlayerState.Idle:

                break;
            case PlayerState.Walk:

                break;
            case PlayerState.Run:

                break;
            case PlayerState.Item:

                break;
        }
    }
}

空の3Dオブジェクトを作成し、このスクリプトをアタッチしておきます。

内容ですが、enum型の変数を作成し、そこにプレイヤーのなりうる状態を羅列しておきます。

currentStateには現在のプレイヤーの状態が格納されます。

currentState = 現在のプレイヤーの状態 というわけですね。

例えば

if(currentState == PlayerState.Run)
{
    // 走り状態のときに実行したい処理を記述
}

こうすることにより、特定の条件下での処理がしやすくなります。

今回したいことのようにアイテムを画面中央で見ているときは、プレイヤーを歩かせたくないし、視点の移動もさせたくないですよね。

その場合、プレイヤーの状態がItemのときは、プレイヤーの移動と視点移動を制限させればいいわけです。

ではさっそく、本題に入りましょう。

 

①プレイヤーの視点が調べられるアイテムに向いているか

こちらに関しては前回の記事でやった、調べられるアイテムを強調表示する際に作成したスクリプトと同じですね。

第2回 Unityで調べられるアイテムを強調表示したい【UnityFx Outline】

前回はRaycastによって取得したゲームオブジェクトのタグがDrawersだったら、アウトラインをつけて強調させるといった内容でした。

そこから流用して、タグがItemでもアウトライン表示させるようにするスクリプト、ItemInteractManagerを作成しました。

以下はそのスクリプトの中身です。

using UnityEngine;

public class ItemInteractManager : MonoBehaviour
{
    public RayManager _rayManager;
    public PlayerStateController _playerStateController;
    [SerializeField] private GameObject rayHitObj = null;
    public GameObject currentItem;

    void switchLayer(GameObject obj, string layerName)
    {
        obj.layer = LayerMask.NameToLayer(layerName);
    }

    void searchCurrentItem()
    {
        rayHitObj = _rayManager.hitObject;
        if(_playerStateController.currentState != PlayerStateController.PlayerState.Item)
        {
            if (rayHitObj != null && rayHitObj.tag == "item")
            {
                resetCurrentItem();
                currentItem = rayHitObj;
                switchLayer(currentItem, "Outline");
            }
            else
            {
                resetCurrentItem();
            }  
        }
    }

    void resetCurrentItem()
    {
        if (currentItem != null)
        {
            switchLayer(currentItem, "Default");
            currentItem = null;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
       
    }


    // Update is called once per frame
    void Update()
    {
        searchCurrentItem();
    }
}

RayManager(前回の記事にあります)、PlayerStateControllerはインスペクターからアタッチしてください。

内容について解説します。

Update関数の中でsearchCurrentItem()が呼び出されていますね。

この関数の役割は今プレイヤーが見ているオブジェクトがItemタグに設定されている場合は、currentItemにそのオブジェクトを代入させてます。

さらに見ているオブジェクトのレイヤーをOutlineに変更してアウトラインを表示させてます。(ここは前回の記事参照)

Itemタグを持っているオブジェクトを画面中央で調べられるようにしたいので、

これで「 ①プレイヤーの視点が調べられるアイテムに向いているか」は、currentItemにオブジェクトが代入されているかで判断できるようになりました。

 

②キー入力でアイテムを画面中央へ移動させる

どのキーにするかはそれぞれですが、私はとりあえずEキーにしておきます。

ItemInteractManagerに記述を追加します。

 

using UnityEngine;
using DG.Tweening; // 追加
public class ItemInteractManager : MonoBehaviour
{
    public RayManager _rayManager;
    public PlayerStateController _playerStateController;
    [SerializeField] private GameObject rayHitObj = null;
    public GameObject currentItem;
    public Camera playerCamera; // 追加
    [SerializeField] private float distance = 0.8f; // 追加
   // アイテムの元の位置
    [SerializeField] private Vector3 itemPos; // 追加
    // アイテムの元の回転
    [SerializeField] private Quaternion itemRot; // 追加
  // アイテムを移動させるポイント
    [SerializeField] private Vector3 pickedItemPos; // 追加
    void switchLayer(GameObject obj, string layerName)
    {
        obj.layer = LayerMask.NameToLayer(layerName);
    }

    void searchCurrentItem()
    {
        rayHitObj = _rayManager.hitObject;
        if(_playerStateController.currentState != PlayerStateController.PlayerState.Item)
        {
            if (rayHitObj != null && rayHitObj.tag == "item")
            {
                resetCurrentItem();
                currentItem = rayHitObj;
                switchLayer(currentItem, "Outline");
            }
            else
            {
                resetCurrentItem();
            }  
        }
    }

    void resetCurrentItem()
    {
        if (currentItem != null)
        {
            switchLayer(currentItem, "Default");
            currentItem = null;
        }
    }
  void observeItem()
    {
        itemPos = currentItem.transform.localPosition;
        itemRot = currentItem.transform.localRotation;
        pickedItemPos = new Vector3(playerCamera.transform.position.x,playerCamera.transform.position.y - 0.2f, playerCamera.transform.position.z) + playerCamera.transform.forward * distance;
        switchLayer(currentItem, "Default");
        _playerStateController.switchState(PlayerStateController.PlayerState.Item);
        currentItem.transform.DOMove(pickedItemPos, 0.3f).SetEase(Ease.OutQuad);
    }
    // Start is called before the first frame update
    void Start()
    {
       
    }


    // Update is called once per frame
    void Update()
    {
        searchCurrentItem();
        // 追加
     if(currentItem != null)
        {

            if(_playerStateController.currentState != PlayerStateController.PlayerState.Item)
            {
                if (Input.GetKeyDown(KeyCode.E))
                {
                    observeItem();
                }
            }
            else if(_playerStateController.currentState == PlayerStateController.PlayerState.Item)
            {
                if (Input.GetKeyDown(KeyCode.E))
                {
                    currentItem.transform.DOLocalMove(itemPos, 0.3f).SetEase(Ease.OutQuad);
                    currentItem.transform.DOLocalRotate(itemRot.eulerAngles, 0.3f).SetEase(Ease.OutQuad);
                    _playerStateController.switchState(PlayerStateController.PlayerState.Idle);
                }
            }
        }
    // 追加ここまで
    }
}

アイテムを移動させるのに、DOTweenを使用しているのでこちらのパッケージはインストールしておいてください。

オブジェクト移動や遅延処理に関して便利すぎるパッケージで、これがなくてはもはやゲームは作れん!!(言い過ぎ)なので!

 

追加分の解説をします。

まずUpdateの中でcurrentItemがnullではないとき(調べられるアイテムを見てるとき)にEキーを押すとobserveItemという関数が走るようになっています。

 

void observeItem()
    {
        // アイテムの元の位置
        itemPos = currentItem.transform.localPosition;
        // アイテムの元の回転
        itemRot = currentItem.transform.localRotation;
        // アイテムを移動させるポイントを取得
        pickedItemPos = new Vector3(playerCamera.transform.position.x,playerCamera.transform.position.y - 0.2f, playerCamera.transform.position.z) + playerCamera.transform.forward * distance;
        switchLayer(currentItem, "Default");
        // ステートの変更
        _playerStateController.switchState(PlayerStateController.PlayerState.Item);
        currentItem.transform.DOMove(pickedItemPos, 0.3f).SetEase(Ease.OutQuad);
    }

observeItemではアイテムを画面中央に持ってくる処理をしていますね。

itemPosとitemRotではもともとのアイテムの位置と回転を取得しています。

アイテムを戻すとき必要になりますので。

pickedItemPosではアイテムの移動先を設定しています。(画面中央に持っていく先)

new Vector3(playerCamera.transform.position.x,playerCamera.transform.position.y – 0.2f, playerCamera.transform.position.z)では、

プレイヤーカメラの位置を取得しています。プレイヤーの目線と同じ場所の座標ですね。

playerCamera.transform.position.y – 0.2f とすることで少しだけ目線より下にしています。

このままだとカメラとオブジェクトが重なった状態になるので、

playerCamera.transform.forward * distanceを足すことで少し前方の位置に設定しています。

0.2fだったり、distanceだったりは各々変更してください。

そしてアイテム確認画面ではアイテムのアウトラインを消したいのでswitchLayerでcurrentItemのレイヤーをDefaultへ変更させます。

switchStateでプレイヤーの状態をItemに設定して、currentItem.transform.DOMoveで実際にpickedItemPosの場所へcurrentItemを移動させます。

 

これで画面中央へアイテムを持ってくることができました。

 

また、アイテムを画面中央で見ているときには、

Eキーを再度押すとアイテムをもとの場所に戻すようになっています。

⑤なんらかのキー入力でアイテム注目状態を解除させる についてもこれで完了ですね。

 

あと、このままだとアイテムを中央に表示したままプレイヤーキャラが動けてしまうので、

 

if(_playerStateController.currentState != PlayerStateController.PlayerState.Item)
{
     // プレイヤーの移動処理や視点移動処理       
}

こんな感じで動きを制限してあげる必要があるでしょう。

各々調整してくださいね。

 

 

③背景にブラーをかけ、光で照らして強調させる

私のように部屋が暗く、光も懐中電灯の先から出している場合は、画像左のように懐中電灯の光があたらずにアイテムが暗くてよくわからないといった感じになってしまいます。

暗い部屋では見えなくて当然というゲーム性でもいいのですが、今回私は画像右のように暗い部屋でも調べられるアイテムははっきりくっきり見えるようにしたいと思います。

ブラー(ぼかし)に関してはなんとなくこっちの方が雰囲気でるので追加してます。

 

特定のアイテムだけを明るく照らす

まずは明るく見えるようにする処理を追加していきます。

プレイヤーの状態がItemのときに、特定のレイヤーを持つオブジェクトだけを照らすライトをプレイヤーの後方に忍ばせておきます。

 

最初にシーンにライトを追加します。

スポットライトを適切な位置に置きましょう。

このようにライトをPlayerオブジェクトの子オブジェクトにすることでプレイヤーが動いてもプレイヤーとの位置関係は変わらずに移動してくれます。

 

ライトを設定できたら、レイヤー編集からレイヤーを追加しましょう。(レイヤーの追加方法は前回の記事参照)

私はLightTargetというレイヤーを追加しました。

 

追加したライトの設定で、カリングマスクを追加したレイヤーだけの状態にします。(他のレイヤーのチェックを外す)

こうすることによって、特定のレイヤーに対してのみ照らすライトが作成できます。

 

あとはスクリプトを変更して完了です。

変更部分はItemInteractManagerのobserveItem関数の中身です。

void observeItem()
    {
        // アイテムの元の位置
        itemPos = currentItem.transform.localPosition;
        // アイテムの元の回転
        itemRot = currentItem.transform.localRotation;
        // アイテムを移動させるポイントを取得
        pickedItemPos = new Vector3(playerCamera.transform.position.x,playerCamera.transform.position.y - 0.2f, playerCamera.transform.position.z) + playerCamera.transform.forward * distance;

        switchLayer(currentItem, "LightTarget"); //変更
        // ステートの変更
        _playerStateController.switchState(PlayerStateController.PlayerState.Item);
        currentItem.transform.DOMove(pickedItemPos, 0.3f).SetEase(Ease.OutQuad);
    }

LightTargetの部分は、追加したレイヤー名にしてください。

こうすることによって、アイテムを調べているときにcurrentItemのレイヤーが変更されライトで照らされるといったわけですね。

 

背景をぼかす

続いてはぼかし処理を行っていきます。

画面のぼかし処理に関しては本来ならPost-Processingパッケージをインスト―ルして行いたかったのですが、

なぜかうちの環境では動かなかったので、URPに標準搭載されているポストプロセスを使用してぼかしを実装しました。(ほんとになんでなんだ…)

 

まずは空のオブジェクトを作成します(CameraEffectという名前にしました)

追記

レイヤーを新しく作成してこのオブジェクトに設定してください(画像では忘れてた)

名前はEffectなどでよいかと。

①インスペクターからVolumeというコンポーネントを追加してください。

②プロファイルを新規に作成してください。

③オーバーライドを追加をクリックして被写界深度(Depth Of Field)を選択してください。

こうするとカメラに映る映像にDoFがかかってぼかせるようになります。(画像のようにチェック外すとぼけないので注意)

フォーカス距離や焦点距離をいじって実際にアイテムを持った時に、アイテムはぼかさないけど、背景はぼかせる値を探してください(力技)

(Post-Processingパッケージが使えればこんな力技いらないのに…)

 

ここまでできたら、被写界深度のチェックは外しておいてください。

プレイヤーの状態がItemのときにだけ、被写界深度のチェックをつけるようにすればいいですね。

 

それではスクリプトを書いていきます。

まずはじめにScreenEffectManagerというものを作成したいと思います。

ポストプロセスによる画面のエフェクトを管理するスクリプトです。

空のオブジェクトを作成して下記のスクリプトをアタッチしてください。

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;



public class ScreenEffectManager : MonoBehaviour
{
    public Volume volume;
    private DepthOfField depthOfField;

    void Start()
    {
        if (volume != null )
        {
            volume.profile.TryGet(out depthOfField);
        }
    }

    void Update()
    {

    }

    public void setDepthOfField()
    {
        if (depthOfField != null)
        {
            depthOfField.active = true;
        }
    }

    public void resetEffect()
    {
        if(depthOfField != null)
            depthOfField.active = false;
    }
}

ポストプロセスを使用するのでこちらの2行を忘れないように!

using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

Volume型変数にはVolumeコンポーネントを持っているオブジェクトをインスペクターからアタッチしてください。

まだDoFしかポストプロセスを使用していないので記述自体は簡素ですね。

 

setDepthOfFieldではDofの設定をアクティブにします。

インスペクターのチェックが付いている状態ってことですね。

resetEffectではDoFの設定を非アクティブ化してます。

 

続いてPlayerStateControllerを変更します。

変更場所はOnEnterStateとOnExitStateの中身です。


using UnityEngine;

public class PlayerStateController : MonoBehaviour 
{
     public ScreenEffectManager _screenEffectManager; // 追加
     public enum PlayerState
     {
         Idle, //止まっている時
         Walk, //歩いている時
         Run, //走っている時
         Item //アイテム確認画面の時
     }
     public PlayerState currentState = PlayerState.Idle;

    // 省略

    void OnEnterState(PlayerState state)
    {
        // 各状態に入ったときの処理を実行
        switch (state)
        {
            case PlayerState.Idle:

                break;
            case PlayerState.Walk:

                break;
            case PlayerState.Run:

                break;
            case PlayerState.Item:
                _screenEffectManager.setDepthOfField(); // 追加
                break;
        }
    }

    void OnExitState(PlayerState state)
    {
        // 各状態から出たときの処理を実行
        switch (state)
        {
            case PlayerState.Idle:

                break;
            case PlayerState.Walk:

                break;
            case PlayerState.Run:

                break;
            case PlayerState.Item:
                _screenEffectManager.resetEffect(); // 追加
                break;
        }
    }

    // 省略

ScreenEffectManager型変数は今までのようにScreenEffectManager(を持つオブジェクト)をインスペクターからアタッチしてください。

 

今回の変更点はcurrentStateがItemになったときとItemではなくなったときに、

ポストプロセスのDofをアクティブにするかしないかの設定ですね。

これでItemを調べているときにDoFがかかるようになります。

 

追記

DoFをかけたい(Effectをかけたい)カメラの環境→ボリュームマスクを、Volumeコンポーネントをもっているオブジェクトのレイヤー名にしてください。

これをしないとカメラにエフェクトがかかりません。

 

④アイテムを回転させられるようにする

最後にアイテムをぐりぐり回転できるようにしたいと思います。

今回変更するのはItemInteractManagerです。

 

using UnityEngine;
using DG.Tweening;
public class ItemInteractManager : MonoBehaviour
{
    public RayManager _rayManager;
    public PlayerStateController _playerStateController;
    [SerializeField] private GameObject rayHitObj = null;
    public GameObject currentItem;
    public GameObject playerObj; // 追加
    public Camera playerCamera;
    [SerializeField] private float distance = 0.8f;

    // 省略

    void Update()
    {
        searchCurrentItem();
        if(currentItem != null)
        {

            if(_playerStateController.currentState != PlayerStateController.PlayerState.Item)
            {
                if (Input.GetKeyDown(KeyCode.E))
                {
                    observeItem();
                }
            }
            else if(_playerStateController.currentState == PlayerStateController.PlayerState.Item)
            {
                if (Input.GetKeyDown(KeyCode.E))
                {
                    currentItem.transform.DOLocalMove(itemPos, 0.3f).SetEase(Ease.OutQuad);
                    currentItem.transform.DOLocalRotate(itemRot.eulerAngles, 0.3f).SetEase(Ease.OutQuad);
                    _playerStateController.switchState(PlayerStateController.PlayerState.Idle);
                }
                // 追加
                if (Input.GetMouseButton(0))  // 0は左クリック
                {
                    float xRot = Input.GetAxis("Mouse X");
                    float yRot = Input.GetAxis("Mouse Y");

                    // プレイヤーのローカルな向きに合わせて回転
                    currentItem.transform.Rotate(playerObj.transform.up, -xRot, Space.World);
                    currentItem.transform.Rotate(playerObj.transform.right, yRot, Space.World);
                }
               // 追加ここまで
            }
        }
    }

変更点はUpdate関数内の部分です。

左クリックしながらマウスを動かすとアイテムが回転するように実装しました。

 

これで冒頭の動画のような感じでアイテムをぐりぐり回転させることができるようになったと思います。

 

まとめ

ゲームでよく見るアイテムぐりぐり回転を実装できました。

しかしこのままでは、もちろん実装した本人しか操作がわからないので、UIも作成して表示させる必要があるでしょう。

センスがない私にとってUIが一番頭を抱えるポイントです…。

 

すらでした。