初心者が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
これでやってみたら問題発生。
原因はカーソルの画像を管理するGameObjectにSpriteRendererを割り当てていたことだった。
SpriteRendererは描画空間がTransformであるため、キャンバスではなくゲーム空間上にカーソルが出現してしまう。
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;
}
}
}
これでアイテムを減らせるようになった。
やってて気づいたのだが、アイテムに所持数上限をつけていない。
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じゃないだろってところを直したり。
とりあえず完了したので今日はこれぐらいで勘弁してやろう。
この記事が気に入ったらサポートをしてみませんか?