#2 Unityでボタンを押した時にアイテムを拾う処理の作り方【Input System対応】

もくじ

前回のおさらい

前回はアイテムオブジェクトを作るところまでやりました。

今回はプレイヤー側の処理を作っていきます。  

プレイヤーがアイテムを拾える範囲を設定する

プレイヤーに範囲設定用の子オブジェクトを作成

HierarchyビューのPlayerArmatureオブジェクトを右クリックしてください。

出てきたメニューの中からCreate Empty左クリック

作成したオブジェクトの名前をItemSerchAreaに変更します。

ItemSerchAreaのInspectorを編集

ItemSerchAreaのInspectorをこのように編集します。

Transform
  • Positionの値を「0,1,1」に変更
BoxCollider
  • Is Triggerにチェック
  • Sizeの値を「3,2,3」に変更
Rigidbody
  • Use Gravityのチェックを外す
  • Is Kinematicにチェック

Input Systemの設定をする

次に、キーボードからの入力を受け取るための下準備をします。

従来Unityでは『Input Manager』という機能を使い様々なプラットフォームの入力に対応していたのですが、今回使用している『Starter Assets – Third Person Character Controller』はUnityに新しく追加された『Input System』という機能を使用しています。

このInput Systemというのは非常によくできていて、従来のInput Managerと比べるとかなり使いやすくなっています。

では早速Input Systemの設定をしていきましょう。

Projectビューから「Assets > StarterAssets > InputSystem」を開いて「StarterAssets」を選択し

Inspectorビューから「Edit asset」をクリックしてください。

Edit assetをクリックすると、このようなウィンドウが出てくると思います。

Actionsの右側にある「」マークを押してActionを追加し、名前を「ItemPickUp」としてください。

キーの設定

ItemPickUpアクションを作成したら、下に表示されている<No Binding>をクリックしてください。

右側に表示されるメニューの中からPathの右側にある空白のドロップダウンをクリックします。

検索窓に「」を入力してください。

出てきた候補の中から一番上の「F [Keyboard]」をクリックしてください。

最後にKeyboardMouseにチェックを入れ、Save Assetをクリックしてください。

これで、ItemPickUpというアクションにFキーを割り当てることが出来ました。

プレイヤーが近場のアイテムを拾う処理を作る

拾う範囲に入ったアイテムを取得する

前回作成したScriptフォルダーに「ItemSerch」スクリプトを作成し、中身に以下のコードを入力してください。

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;

public class ItemSerch : MonoBehaviour
{
    [SerializeField, Header("アイテムを取得する原点(これを元に近いアイテム、遠いアイテムが決まる)")] private GameObject originPoint;

    public List<GameObject> ItemList { get; private set; } = new List<GameObject>();

    [Header("アイテムリストを常に更新")]
    public bool isAlwaysUpdate = true;
    // アイテムリストが更新されたか
    private bool isItemListUpdate;

    private void Start()
    {
        // アイテム削除関数を実行開始
        StartCoroutine(LateFixedUpdate());
    }

    /// <summary>
    /// オブジェクトと接触した時呼ばれる
    /// </summary>
    /// <param name="other"></param>
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Item"))
        {
            isItemListUpdate = true;
            var item = other.GetComponent<Item>();
            // アイテムを拾った時アイテムリストから除外するイベントを登録
            item.onPickUp.AddListener(() => ItemList.Remove(other.gameObject));
            item.onPickUp.AddListener(() => isItemListUpdate = true);
            ItemList.Add(other.gameObject);
        }
    }

    /// <summary>
    /// オブジェクトが離れた時呼ばれる
    /// </summary>
    /// <param name="other"></param>
    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Item"))
        {
            isItemListUpdate = true;
            ItemList.Remove(other.gameObject);
        }
    }

    /// <summary>
    /// FixedUpdateの実行タイミングで一番最後に実行される関数
    /// </summary>
    /// <returns></returns>
    private IEnumerator LateFixedUpdate()
    {
        var waitForFixed = new WaitForFixedUpdate();
        while (true)
        {
            // 常に更新フラグが立っているか、アイテムリストが更新された時だけ要素の入れ替えを実行
            if (isAlwaysUpdate || isItemListUpdate)
            {
                PickUpNearItemFirst();
                isItemListUpdate = false;
            }
            yield return waitForFixed;
        }
    }

    /// <summary>
    /// 一番近場のアイテムを配列の先頭に持ってくる
    /// </summary>
    /// <returns></returns>
    private void PickUpNearItemFirst()
    {
        if (ItemList.Count <= 1) return;

        var originPos = originPoint.transform.position;
        // 初期最小値を設定
        var minDirection = Vector3.Distance(ItemList[0].transform.position, originPos);
        // 二つ目のアイテムから取得ポイントとの距離を計算
        for (int itemNum = 1; itemNum < ItemList.Count; itemNum++)
        {
            var direction = Vector3.Distance(ItemList[itemNum].transform.position, originPos);
            // より近いオブジェクトを0番目の要素に代入
            if (minDirection > direction)
            {
                minDirection = direction;
                var temp = ItemList[0];
                ItemList[0] = ItemList[itemNum];
                ItemList[itemNum] = temp;
            }
        }
    }

    /// <summary>
    /// 一番近いアイテムを返す
    /// </summary>
    /// <returns></returns>
    public GameObject GetNearItem()
    {
        if (ItemList.Count <= 0) return null;

        return ItemList[0];
    }

#if UNITY_EDITOR // UnityEditorのみ
    /// <summary>
    /// 拾う対象のアイテムにギズモを表示
    /// </summary>
    private void SetPickUpTargetItemMarker()
    {
        var item = GetNearItem();
        if (item == null) return;
        Gizmos.DrawSphere(item.transform.position, 0.1f);
    }
    private void OnDrawGizmos()
    {
        SetPickUpTargetItemMarker();
    }
#endif
}

このスクリプトの説明を簡単にすると

  1. OnTriggerEnterで接触したアイテムをItemListに登録
  2. OnTriggerExitで離れたアイテムをItemListから除外
  3. LateFixedUpdateでItemListが更新された時、先頭要素を指定した地点と一番距離が近いアイテムにする

です。

アイテムが拾われた時もItemListからアイテムを除外する為、UnityEventを使用してイベントを設定しています。

また、工夫ポイントとして毎回要素の比較・入れ替えを行うとかなり無駄な処理(オーバーヘッド)が発生してしまうので、ItemListが更新された時のみ要素の比較・入れ替えを行うようにしています。

※追記 2021/6/28
この工夫ポイントを盛り込むと若干タイトル詐欺になってしまうことに気付きました。。。orz
なので、常に更新するかどうかをフラグでOn Off切り替えられるようコードを変更しました。

ちなみにItemListの中身を比較・入れ替えするメソッドは「PickUpNearItemFirst」です。

ItemSerchAreaのInspectorを編集

先ほど作成した「ItemSerch.cs」をHierarchyビューにあるPlayerArmatureの子オブジェクトである
ItemSerchArea」にドラッグ&ドロップでアタッチしてください。

itemSerchAreaにスクリプトをアタッチすると、「Origin Point」というプロパティがInspectorに表示されていると思います。

その場所にPlayerArmatureの子オブジェクトである「PlayerCameraRoot」というオブジェクトを入れてください。

このOrigin Pointというのは、どの場所を基準にアイテムとの距離を測るかの基準点となります。

一度アイテムを置いてシーンを再生してみましょう!

前回作成したItemプレハブをシーンに出してみましょう。

Assets > Prefab にあるItemをHierarchyの空いている空白部分にドラッグ&ドロップしてください。

生成したItemプレハブのPositionを「0,1,3」に変更してください。

Gameビューにある「Maximize On Play」「Gizmos」という二つのボタンをクリックして有効にしてください。他より若干白くなっていれば有効になっています。

薄っすらと見える緑色の線がプレイヤーがアイテムを拾うことのできる範囲です。

その範囲にアイテムが入るとアイテムに灰色の球が表示されるのが確認できるかと思います。

プレイヤーがボタンを押したらアイテムを拾う

Scriptフォルダーに「Item Pick Up」スクリプトを作成して、以下を入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using StarterAssets;

public class ItemPickUp : MonoBehaviour
{
    [SerializeField] private ItemSerch itemSerch;

    /// <summary>
    /// ItemPickUpにバインドされているボタンが押されたら呼ばれる
    /// </summary>
    public void OnItemPickUp()
    {
        var item = itemSerch.GetNearItem();
        if (item == null) return;
        item.GetComponent<Item>().PickUp();
    }
}

OnItemPickUpという関数は前に作成したItemPickUpアクションに割り当てたキーが押された時呼ばれます。

これはUnityのSend Messageという機能を使用して呼ばれます。

詳しくは「Input System」「Player Input」「SendMessage」等で調べると色々出てくると思います。

ItemPickUpスクリプトをプレイヤーに付ける

作成したスクリプトをHierarchyビューのPlayerArmatureにドラッグ&ドロップでアタッチしてください。

ItemPickUpのプロパティにItem Serchという場所が出てくると思います。

そこにHierarchyビューからPlayerArmatureの子オブジェクトであるItemSerchAreaをドラッグ&ドロップで入れてください。

アイテムが拾えるかどうかを確認

再生して確認してみましょう。

アイテムを拾うには近づいて灰色の球が出たところで「F」キーです。

拾うとConsoleビューに「拾われた!」と出てくるのが確認できると思います。

アイテムをシーンに配置しよう

ここまで出来たらあとはアイテムをシーンに配置していくだけです。

HierarchyビューのItemを選択した状態で、「Ctrl + D」を押すと複製できます。

自分は試しに500個ほど置いてみました。

結構置きましたが300FPSを切ることなく動いています。

さいごに

いかがだったでしょうか?

二つの記事に渡って紹介してきましたが、もう一つくらいに分けてもいい長さだったかなと後になって思ってます(笑)

今回はアイテムに特に種類が無かったのでItemクラスを一つだけ作りましたが、本来は色々な種類のアイテムが存在すると思います。

そういった場合はItemクラスを抽象クラスなどにしてそれを各アイテムクラスに継承させれば、拾う処理に関してはコードを変えることなくそのまま使えるので結構綺麗に作れたかなと思います。

あとこれにはまだ問題があり、例えば以下のようなシーン

はい。壁の向こう側のアイテムが拾えてしまいます(笑)

これを防ぐ一番簡単な手段としては、基準点からアイテムに向けてレイを飛ばして、アイテム以外のオブジェクトに当たっていたら拾えないとすることです。

このあたりに関しては気が向いたら記事にして出すかもしれません。

というわけでここまでお疲れ様でした。

次回の記事に関してはまだなんも考えてないのですが、あんまり期間を開けないようボチボチやっていこうと思います!

それでは、さいなら~

2021/6/28

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です