見出し画像

初心者がUnityでゲーム作る Day12 ~ SpineのEvent取得、落とす

Day12まとめ

  • 足音が鳴るようになった

  • 見た目が変更可能なゲーム内カーソルを作った

  • アイテム欄のアイテムを減らす機構ができた

  • アイテム欄のアイテムにカーソルを合わせると、マップに落とせるようになった

足音を鳴らしたい

無音で走っていて寂しいので足音を実装しておく。

Unity備え付けのAnimatorなら、GUI上で足音再生の関数を紐づけるだけで実装できる。しかし、今回はSpineのライブラリを用いてアニメーションを呼び出しているのでその手は使えない。

ではどうするかというと、Spine側で設定した「Event」を使う。
SpineでのAnimation作成時、足が地面につくタイミングで「Footstep」というEventが走るように設定してある。
これをUnity側で取得し、足音再生の関数を実行する。

イベント取得側

using UnityEngine;
using Spine.Unity;
public class PlayerAnimationController : MonoBehaviour
{
    // アニメーション変数の宣言
    // Spine.AnimationState and Spine.Skeleton are not Unity-serialized objects. You will not see them as fields in the inspector.
    public Spine.AnimationState animationState;
    public Spine.Skeleton skeleton;
    private SkeletonAnimation skeletonAnimation;

    // Eventの取得用
    private string targetEventName = "Footstep";
    Spine.EventData targetEventData;

    // 音声再生用インスタンス
    public FootStepSEPlayer footStepSEPlayer;

    void Start()
    {
        // Make sure you get these AnimationState and Skeleton references in Start or Later.
        // Getting and using them in Awake is not guaranteed by default execution order.
        skeletonAnimation = GetComponent<SkeletonAnimation>();
        animationState = skeletonAnimation.AnimationState;
        skeleton = skeletonAnimation.Skeleton;

        // イベントデータをキャッシュすることで、文字列の比較を省略することができます
        targetEventData = skeletonAnimation.Skeleton.Data.FindEvent(targetEventName);

        // あらゆるアニメーションから発生されるイベントに対して登録する
        // ここの書き方はこちらから https://ja.esotericsoftware.com/spine-unity-main-components
        animationState.Event += OnUserDefinedEvent;
    }

    public void OnUserDefinedEvent(Spine.TrackEntry trackEntry, Spine.Event e)
    {
        if (e.Data == targetEventData)
        {
            // ユーザー定義イベントに反応させたい実装コードをここに追加してください
            Debug.Log("足音!");
            footStepSEPlayer.PlayFootStepSE();
        }
    }

足音再生側

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

// 効果音再生 参考
// https://acua-piece.com/post/186042580240/game-audio-with-unity-footsteps
[RequireComponent(typeof(AudioSource))]
public class FootStepSEPlayer : MonoBehaviour
{
    [SerializeField]
    AudioClip[] clips;

    [SerializeField]
    float pitchRange = 0.1f;
    float pitchUp = 0.5f;
    protected AudioSource source;

    private void Awake()
    {
        source = GetComponents<AudioSource>()[0];
    }

    public void PlayFootStepSE()
    {
        // ピッチを1.0~1.1fの間でランダムにしてバラつき感を出す
        source.pitch = 1.0f + Random.Range(-pitchRange + pitchUp, pitchRange + pitchUp);

        // 複数設定したクリップから、SerializeFieldのLength(4ファイルなら4つ)の数を抽選して渡す
        source.PlayOneShot(clips[Random.Range(0, clips.Length)]);
    }
}


足踏みのタイミングで処理が走っているのがわかる

イベントがある時に全部反応するようになっているので、Switch文で振り分けるなどすることで、いろんなイベントに対応してSEが鳴らしわけられそうだ。今は足音だけで我慢しておく。

アイテムを落としたい

アイテム欄を触る基本システムとして以下を作る。

  • アイテムを取り出してマップに置く機構

  • アイテム欄の整理

ストレージの実装など後に控えた仕様の基盤となる部分だ。
いずれもItemBox回りの処理となるため、一緒に設計しておく。
やっていきましょう。

実装する仕様

  • アイテムを1個ずつ落とす

    • アイテム欄で選択中の欄に格納されたアイテムが「おとせるやつ」である場合、以下を行う

      • アイテムアイコンがカーソルについてくる

      • マップ上でクリックするとその場にアイテムが1個生成される

      • 所持数が1個減る

クラス設計「アイテムを落とせる」

CursorChaser

カーソルを追わせる。作り方はググる。

CursorRenderer

以下の関数を仕込んでおく。

  • spriteを受け取って切り替え

    • スプライトはitemIDで検索

      • 受け取ったidが「0_none」だったら

      • デフォルトのカーソル画像に切り替え

ItemBoxSelectSwitcher

  • 選択中ボックスIDの計算をして結果を戻す

    • ItemBoxInputから切り出し

ItemBoxInput(作成済み・リファクタ)

キー入力を受けて色々なプログラムを発火させるだけの存在にする。

  • マウス入力があったとき

    • int 入力 に値を入れる

      • ホイールアップで-1

      • ホイールダウンで+1

    • 各種関数に引数をバラまいて発火

      • ItemBoxSelectSwitcher(db.選択中系列の色々)

      • CursorRenderer(db.選択中ボックスのitemID)

ItemBoxWithdrawer

アイテムボックスからアイテムが減少する処理を書く

  • boolを返す

  • 減らしたいアイテム種と個数を受け取る

    • 足りてますかチェック

      • アイテム種で検索

      • 個数が足りているか、ボックス内の個数を全部合算して確認

        • 足りていない場合

          • コンソールに警告出す

          • return false

        • 足りている場合

          • 減らすやつをやる

    • 減らすやつ

      • 同じアイテムが入っているボックス番号を全部取得

        • for文で減算処理開始

        • 減らすべき個数が残っている場合、

          • 右のボックスから順に該当するアイテムの個数を減らす

        • 減らすべき個数が0になった場合、

          • 表示更新を走らせる

          • コンソールにやったよって出す

          • return true

EquipManager

カーソルがセットされているアイテムを「Equip」と呼ぶ
Equipのやれることリストを書いていく

  • ItemKindがResourceである場合、String "disposable"

    • Equip増えてきたらリファクタ

      • Equip側に機能を持たせるようにする

Disposable用の関数

  • フラグが"disposable"の時、以下を行う

    • ItemWithdrawerに減少数1を渡して減少実行

      • trueが帰ってきている

        • ItemDroppterにtransformを渡して複製実行

      • falseが返ってきている

        • 警告を出し、なにもしない

OnClickObjectHandler(作成済み・リファクタ)

クリック時の挙動がめっちゃ伸びてきたのでどっかでリファクタが必要だ。

  • クリック時にぶつかった場所のtransformを取得

    • EquipManagerに渡して、1個落とす処理を発火

  • DropItemAnimator(作成済み・追加)

    • 新規にいい感じのアニメーションを設定する

ItemDropper(作成済み・リファクタ)

ハードコードされている。
(岩(Harvestavble)をクリックしてドロップアイテムをInstantiate)
改修して以下のようにする。

  • objSpawnFromのタグによってSwitch

    • タグ:Harvestable

      • 今のまま

    • タグ:Equip

      • 新しい落とす処理

  • プロパティの取得元

  • アニメーション

OnClickObjectHandlerでタグを読み、このクラスを発火させてまたタグを読むという運用になってしまっている。
無駄が多い。
またリファクタすることになるだろう。

実装

CursorChaser / CursorRenderer

これでやってみたら問題発生。

SceneView上では問題なく表示されている
GameViewではクソデカカーソルが世界を覆ってしまう

原因はカーソルの画像を管理するGameObjectにSpriteRendererを割り当てていたことだった。
SpriteRendererは描画空間がTransformであるため、キャンバスではなくゲーム空間上にカーソルが出現してしまう。
Imageコンポーネントにしなければいけなかったようだ。

Imageに差し替えたらちゃんとカーソルになった

システム標準のカーソルは以下のやり方で非表示にできる。

ItemBoxInput / ItemBoxSelectSwitcher

InputをマジでInputだけにした。

public class ItemBoxInput : MonoBehaviour
{
    public ItemBoxSelectSwitcher selectSwitcher;
    int wheelInput;

    // Update is called once per frame
    void Update()
    {
        // アイテム欄の左右移動処理
        if (Input.GetAxis("Mouse ScrollWheel") > 0) // ホイールダウン
        {
            wheelInput = -1;
            selectSwitcher.SwitchSelect(wheelInput);
        }

        if (Input.GetAxis("Mouse ScrollWheel") < 0) // ホイールアップ
        {
            wheelInput = 1;
            selectSwitcher.SwitchSelect(wheelInput);
        }
    }
}

スッキリ!
クラスの分け方が分かってきた気がする。

ItemBoxWithdrawer

ボックスの中身を検索してアイテムを減らしてくるという関数になる。
その性質上、「foreach」を使うと実装がやりやすそうだろうということで、資料を探してみた。

listやらarrayやらも出てくるので、おさらいしておく。

arrayとListは似ているが同じような操作の命令方法が全く異なる。

  • 長さの取得

    • array.Length

    • list.Count

  • 要素の追加

    • array[i] = n

    • list.Add(i)

記事にある通り要素数は0スタートなのに注意する。
array[0],array[1]……てな具合。

作ったコードはこちら。

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

public class ItemBoxWithdrawer : MonoBehaviour
{
    // インスタンス化
    public ItemBoxDataBase db;
    public ItemBoxViewRefresher refresher;
    public ItemManager itemManager;

    // 引き出し管理用
    private int stock;
    private int[] withdrawBoxArray;

    // 在庫足りてるかチェック
    public bool CheckStock(string demandId, int demandNum)
    {
        // Withdraw受け渡し用のArray初期化
        var checkedBox = new List<int>();
        withdrawBoxArray = new int[0];
        stock = 0;
        print($"在庫チェック開始 : ItemID: {demandId} / {demandNum}個");

        // Box1個ずつ入ってるアイテムをチェック
        foreach (var boxID in db.storagedItems.Keys)
        {
            // id一致チェック
            if (demandId == db.storagedItems[boxID].Item1)
            {
                // 在庫あるねのカウントに加算
                stock += db.storagedItems[boxID].Item2;

                // チェック対象に追加
                checkedBox.Add(boxID);

                // 在庫が足りている時の処理
                if (stock >= demandNum)
                {
                    Debug.Log("在庫OK");

                    // 末尾のBoxから順に引き出し対象Boxに追加
                    checkedBox.Reverse();
                    withdrawBoxArray = new int[checkedBox.Count];
                    print($"引き出し対象ボックス数: {withdrawBoxArray.Length}個");

                    // 引き出しボックスの設定
                    int withdrawID = 0;
                    foreach (int i in checkedBox)
                    {
                        withdrawBoxArray[withdrawID] = i;
                    }
                    return true;
                }
            }
        }

        // 全Boxで在庫確認終わって足りないならfalseを返す
        Debug.Log("在庫NG");
        return false;
    }

    public bool Withdraw(string demandId, int demandNum)
    {
        // 在庫チェックで足りてなかったらエラーで戻す
        if (CheckStock(demandId, demandNum) == false)
        {
            Debug.Log("在庫不足でアイテム引き出しに失敗");
            return false;
        }
        else
        {
            // 1ボックスずつ引き出す
            foreach (int i in withdrawBoxArray)
            {
                int ownNum = db.storagedItems[i].Item2;
                var b = db.storagedItems[i];

                // そのboxに引き出せる十分量ある場合
                if (ownNum > demandNum)
                {
                    // 所持数を要求数分減らして終了
                    ownNum += -demandNum;
                    db.storagedItems[i] = (b.Item1, b.Item2 - demandNum, b.Item3, b.Item4, b.Item5);
                    refresher.RefreshBoxView(i);
                    return true;
                }
                // 足りていない場合
                else
                {
                    // 要求数を減らしてからboxの中身をカラにする
                    demandNum += -ownNum;
                    db.storagedItems[i] = (
                        "0_None",
                        0,
                        itemManager.SearchKind("0_None"),
                        itemManager.SearchName("0_None"),
                        itemManager.SearchInfo("0_None")
                    );
                    ;
                    refresher.RefreshBoxView(i);
                }
            }
            Debug.Log("謎の要因でアイテム引き出しに失敗");
            return false;
        }
    }
}
適当に組んだボタンで石を3個ずつ減らすと、所持数不足の時にちゃんと止まってくれた

これでアイテムを減らせるようになった。
やってて気づいたのだが、アイテムに所持数上限をつけていない。
intの上限個の石を持ててしまうし、自動でスタックするように機能を作ってしまったので複数欄に同じアイテムがある時の機能検証ができない。
今後なんとかせねばならない。

もう一つ気づいたが、このままだと複数欄に跨ってアイテムを持っているときに一番最後の欄のものから消費される。
所定のボックスの在庫を減らす関数を組んでおく。

    // 特定のボックスを指定して引き出す場合のチェック処理
    public bool CheckStockSingleBox(int demandNum, int boxID)
    {
        {
            print($"在庫チェック開始 : {demandNum}個 / BoxID {boxID}");
            if (db.storagedItems[boxID].Item2 >= demandNum)
            {
                Debug.Log("在庫OK");
                return true;
            }
            else
                Debug.Log("在庫NG");
            return false;
        }
    }

    // 特定のボックスを指定して引き出す処理
    public bool WithdrawSingleBox(int demandNum, int boxID)
    {
        // 在庫チェック
        if (CheckStockSingleBox(demandNum, boxID) == true)
        {
            var b = db.storagedItems[boxID];

            // 要求数ピッタリだった場合、無をセット
            if (demandNum == b.Item2)
            {
                db.storagedItems[boxID] = (
                    "0_None",
                    0,
                    itemManager.SearchKind("0_None"),
                    itemManager.SearchName("0_None"),
                    itemManager.SearchInfo("0_None")
                );
            }
            // 余りが出る場合、数だけ変更
            else
            {
                db.storagedItems[boxID] = (b.Item1, b.Item2 - demandNum, b.Item3, b.Item4, b.Item5);
            }
            refresher.RefreshBoxView(boxID);
            return true;
        }
        else
            Debug.Log("謎の要因でアイテム引き出しに失敗");
        return false;

ItemBoxSelectSwitcher

カーソルがデフォルトのままだと、「いま石選んでます落とせます状態」になったことに気づきづらい。
ItemBoxの選択を変えたらカーソルが変わるようにする。

追記

    void BoxSelectEnd()
    {
        string id = db.storagedItems[db.SelectedBoxID].Item1;
        if (id == "0_None")
        {
            cursorRenderer.CursorReset();
        }
        else
            cursorRenderer.CursorSetter(itemManager.SearchSprite(id));
        // 移動後のアイテム欄をアクティブ化
        refresher.ShowSelectedBoxObj();
ヨシ


閑話休題 装飾系

岩打撃音

岩を殴っても音がしないのがさみしくなってきたので音を導入した。
Footstepとほぼ同じなので実装は割愛。

フォント

デフォルトのTextMeshProでは日本語が表示できないので日本フォントを入れる。

実装に戻る

EquipManager / OnClickObjectHandler / ItemDropper

クリックした場所にアイテムを落とす機構を作っていく。
ここは一旦ハードコード気味に作るのでバグりさえしなければよい。

依存関係としては、

OnClickObjectHandler
↓発火
EquipManager
↓発火
ItemDropper

という感じ。
こういうのクラス図にしちゃった方がいいのかもなと思わんでもない。
どうせこのブログで構造とか変数とかバリバリ書いてるしなあ。

OnclickObjectHandler追記


                // EquipがResourceである場合、ポトポト落とす関数を実行する
                // todo ifじゃなくす
                if (equipManager.CheckEquipKind() == "Disposable")
                {
                    equipManager.Disposable();
                }
                // EquipがResourceでない場合
                else

if文で強引に呼び出す。

ItemDropper追記

    public (string, int, string) FetchProperty(GameObject objSpawnFrom)
    {
        // 何か入れないと怒られるのでダメそうな数値を入れておく
        string id = "0_None";
        int num = 0;
        string setAnim = "N/A";

        switch (objSpawnFrom.tag)
        {
            // Harvestableからスポーンさせる時
            case "Harvestable":
                // コンポーネントにくっついてるドロップアイテム情報を取得してくる
                var harvestableDropTable = objSpawnFrom.GetComponent<HarvestableDropTable>();
                id = harvestableDropTable.HarvestableItemID;
                num = harvestableDropTable.HarvestableNum;
                setAnim = jumpAnimTag;
                break;

            // Equipからスポーンさせる時
            case "Equip":
                var equipManager = objSpawnFrom.GetComponent<EquipManager>();

                // アクティブなアイテム欄のアイテムを1個生成する
                // todo たぶんDisposableであるならばとか必要
                var a = db.storagedItems[db.SelectedBoxID];
                id = a.Item1;
                num = 1;
                setAnim = fallAnimTag;
                break;
        }
        return (id, num, setAnim);
    }

DropItemCreate(追記部分)


        // 生成するドロップアイテムに持たせる情報を取得
        (string spawnedItemID, int spawnedItemNum, string setAnim) = FetchProperty(objSpawnFrom);

        // アニメーション
        if (setAnim == jumpAnimTag)
        {
            animator.MinedJump(newObj);
        }
        else if (setAnim == fallAnimTag)
        {
            animator.Fall(newObj);
        }

カーソルにEquipタグのついたオブジェクトを設置し、オブジェクト自体を引数で渡して発火するように設定した。

EquipManager

public class EquipManager : MonoBehaviour
{
    // todo リファクタして殺害し、Equip側に流す

    public ItemBoxDataBase db;
    public ItemBoxWithdrawer withdrawer;
    public ItemDropper dropper;

    // Equipの種類を指定する
    // todo いろんな種類に対応させる
    public string CheckEquipKind()
    {
        print($"選択中Box{db.SelectedBoxID}");
        var kind = db.storagedItems[db.SelectedBoxID].Item3;

        if (kind == Item.ItemKind.Resource)
        {
            return "Disposable";
        }
        else
            return "N/A";
    }

    public void DisposeToField()
    {
        if (withdrawer.WithdrawSingleBox(1, db.SelectedBoxID) == true)
        {
            Debug.Log("アイテム減算成功");
            dropper.dropItemCreate(gameObject);
            return;
        }
        else
        {
            Debug.Log("アイテム減算失敗によりドロップ処理を中断");
            return;
        }
    }
}

アイテムに設定された種類がResourceであるなら、
WithdrawSingleBoxを実行し、
実行した結果Trueなら、
このオブジェクトを渡してDropItemCreateを実行する。

実行した結果……

画像中央下にある

「このオブジェクト」(UI画像)に生成された。
そうか。Rayがぶつかった地点のトランスフォームを渡さないとダメか。

ということで書き換える。

RayからTransformを取得すればよいということなのだが、
今回の関数はだいたいGameObjectでやり取りをしてしまっている。

ということで、Rayが当たった地点にゲームオブジェクトを生成してみることにする。

                print($"Rayが{hit.point}にヒット");
                // Rayの当たった地点に情報のやり取りをするためのGameObjectを生成
                GameObject rayHitSpotObj = Instantiate(
                    _rayHitSpotObj,
                    hit.point,
                    Quaternion.identity
                );
生成されるようになった

生成確認したので関数の末尾にDestroyと書いておく。
書かないと無限に溜まっていく。

このゲームオブジェクトを利用して情報の受け渡しを行う。
今回はTag「Equip」を渡せばよい

                // EquipがResourceである場合、ポトポト落とす関数を実行する
                if (equipManager.CheckEquipKind() == "Disposable")
                {
                    rayHitSpotObj.tag = "Equip";
                    equipManager.DisposeToField(rayHitSpotObj);
                }
ボトボトボト

実装完了!

まだ色々対応が必要そうな感じはする。
アニメーションをつけたり、もはやTagがEquipじゃないだろってところを直したり。

とりあえず完了したので今日はこれぐらいで勘弁してやろう。

この記事が気に入ったらサポートをしてみませんか?