構想の 5/7 ステップに来ました
毎回更新を見てくれているみんなにはなじみの構想記事
simplestar-tech.hatenablog.com
今回は前回 4.5 ステップとしてやるべきと示したブロックの破壊と配置によるアイテムスロットの操作と
その時に行われるリクエストの反映、オーバースロット時の挙動を載せておきます。
アイテムデータの初回取得
プレイヤーはゲームが始まってすぐにブロックを破壊したがるものですが、そのときに自身のアイテム情報をすべて知っておっく必要があります。
ゲーム開始時にアイテムを取得しておくことにします。
インベントリを開いたときは諸々ロックする
まずインゲームでは Ctrl キーを押したときにカメラ回転が固定され、マウスのカーソルが現れカーソル移動できるようになります
もう一度 Ctrl キーを押すと、マウス移動でカメラ回転が行われるようになり、マウスカーソルが消えます
インゲーム中は後者のマウスカーソルが出ていないときにのみ E キーでインベントリが表示されるようになり
Ctrl キー同様にカメラ回転が固定され、マウスカーソルが現れるようになります
Ctrl キーはこのとき押しても効果はありません
このあたりは InputModeSwitcher あたりに任せられないだろうか?
ゲームステートを StateMachine で
AnimatorController にアクセスすると現在のステートを知ることができる
ビジュアライズと遷移のロジックをこの AnimatorController に任せることはできないだろうか?
できる
別記事を書く
書いたよ
simplestar-tech.hatenablog.com
こちらをゲームに組み込むことにしました。
ステートの切り替え
ゲームロジックは上記のステートマシンの現在のステートによって切り替わるとして
具体的には
Explore モードのときに
R Ctrl キーを押すと、カメラ回転がロックされてカーソルが現れ、カーソルの先にあるキューブがもこもこします
もう一度 R Ctrl キーを押すか Esc キーを押すと Explore モードに戻ってきます(もこもこできなくなり、カーソルが消えて、カメラが自由回転する)
Explore モードのときに
T キーを押すと、テキストボックスが現れて、テキストを打ち込めます。(このときあらゆるキー操作が無効になりテキストの入力のみ行える)
Enter を押すか Esc キーを押すことで Explore モードに戻ってきます
Explore モードのときに
E キーを押すと、インベントリが現れ、カメラ回転がロックされて、カーソルが現れ、カーソルの先にあるスロットアイテムをホールドしてスロット移動できるようになります。
もう一度 Eキーを押すか、Esc キーを押すとインベントリが消え、カーソルが消え、カメラが自由回転できるようになり、Explore モードに戻ってきます
を実装して動作するか見てみます。
具体的な実装はこんな感じになりました
using Cinemachine;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
<summary>
</summary>
public class InputModeStateMachine : MonoBehaviour
{
#region SceneComponents
[SerializeField] Transform vrmCharacters = null;
[SerializeField] CinemachineFreeLook cinemachineFreeLook = null;
[SerializeField] TextChatGUI textChatGUI = null;
#endregion
#region Assets
[SerializeField] Texture2D cursorImage;
#endregion
<summary>
</summary>
internal UnityAction<InputModeState, InputModeState> onChangeInputMode;
<summary>
</summary>
public InputAction miningAction;
public InputAction inventoryAction;
public InputAction textChatAction;
public InputAction escapeAction;
void Start()
{
this.animator = GetComponent<Animator>();
for (InputModeState stateEnum = InputModeState.Explore; stateEnum < InputModeState.Max; stateEnum++)
{
var fullPath = INPUT_STATE_LAYER_NAME + "." + stateEnum.ToString();
var hash = Animator.StringToHash(fullPath);
this.stateHashToEnum.Add(hash, stateEnum);
}
this.LockFreeLook(lockFreeLook: false);
this.SetCursorImage(this.cursorImage);
this.miningAction.performed +=
ctx =>
{
var stateInfo = this.animator.GetCurrentAnimatorStateInfo(INPUT_STATE_LAYER_INDEX);
var currentState = this.stateHashToEnum[stateInfo.fullPathHash];
switch (currentState)
{
case InputModeState.Explore:
this.animator.SetTrigger(InputModeState.Mining.ToString());
this.LockUnlockCursor(true);
this.LockFreeLook(true);
this.onChangeInputMode?.Invoke(currentState, InputModeState.Mining);
break;
case InputModeState.Mining:
this.animator.SetTrigger(InputModeState.Explore.ToString());
this.LockUnlockCursor(false);
this.LockFreeLook(false);
this.onChangeInputMode?.Invoke(currentState, InputModeState.Explore);
break;
case InputModeState.TextChat:
break;
default:
break;
}
};
this.inventoryAction.performed +=
ctx =>
{
var stateInfo = this.animator.GetCurrentAnimatorStateInfo(INPUT_STATE_LAYER_INDEX);
var currentState = this.stateHashToEnum[stateInfo.fullPathHash];
switch (currentState)
{
case InputModeState.Explore:
this.animator.SetTrigger(InputModeState.Inventory.ToString());
this.LockUnlockCursor(true);
this.LockFreeLook(true);
this.onChangeInputMode?.Invoke(currentState, InputModeState.Inventory);
break;
case InputModeState.Inventory:
this.animator.SetTrigger(InputModeState.Explore.ToString());
this.LockUnlockCursor(false);
this.LockFreeLook(false);
this.onChangeInputMode?.Invoke(currentState, InputModeState.Explore);
break;
case InputModeState.TextChat:
break;
default:
break;
}
};
this.textChatGUI.onEndEditMessage += OnEndEditMessage;
this.textChatAction.performed +=
ctx =>
{
var stateInfo = this.animator.GetCurrentAnimatorStateInfo(INPUT_STATE_LAYER_INDEX);
var currentState = this.stateHashToEnum[stateInfo.fullPathHash];
switch (currentState)
{
case InputModeState.Explore:
this.animator.SetTrigger(InputModeState.TextChat.ToString());
this.LockUnlockCursor(true);
this.onChangeInputMode?.Invoke(currentState, InputModeState.TextChat);
break;
case InputModeState.TextChat:
break;
default:
break;
}
};
this.escapeAction.performed +=
ctx =>
{
var stateInfo = this.animator.GetCurrentAnimatorStateInfo(INPUT_STATE_LAYER_INDEX);
var currentState = this.stateHashToEnum[stateInfo.fullPathHash];
switch (currentState)
{
case InputModeState.Explore:
case InputModeState.TextChat:
break;
default:
this.animator.SetTrigger(InputModeState.Explore.ToString());
this.LockUnlockCursor(false);
this.LockFreeLook(false);
this.onChangeInputMode?.Invoke(currentState, InputModeState.Explore);
break;
}
};
}
<summary>
</summary>
private void OnEndEditMessage(string text)
{
this.animator.SetTrigger(InputModeState.Explore.ToString());
this.LockUnlockCursor(false);
this.onChangeInputMode?.Invoke(InputModeState.TextChat, InputModeState.Explore);
}
public void OnEnable()
{
this.miningAction.Enable();
this.inventoryAction.Enable();
this.textChatAction.Enable();
this.escapeAction.Enable();
}
public void OnDisable()
{
this.miningAction.Disable();
this.inventoryAction.Disable();
this.textChatAction.Disable();
this.escapeAction.Disable();
}
<summary>
</summary>
<param name="lockCursor"></param>
void LockUnlockCursor(bool lockCursor)
{
var vInput = this.vrmCharacters.GetComponentInChildren<Invector.vCharacterController.vThirdPersonInput>();
if (null != vInput)
{
vInput.ShowCursor(lockCursor);
vInput.LockCursor(lockCursor);
if (!lockCursor)
{
this.SetCursorImage(this.cursorImage);
}
}
}
<summary>
</summary>
<param name="lockFreeLook"></param>
void LockFreeLook(bool lockFreeLook)
{
var axisNameX = "";
var axisNameY = "";
if (lockFreeLook)
{
this.cinemachineFreeLook.m_XAxis.m_InputAxisValue = 0;
this.cinemachineFreeLook.m_YAxis.m_InputAxisValue = 0;
}
else
{
if (null != Gamepad.current)
{
axisNameX = "RightAnalogHorizontal";
axisNameY = "RightAnalogVertical";
}
else
{
axisNameX = "Mouse X";
axisNameY = "Mouse Y";
}
}
this.cinemachineFreeLook.m_XAxis.m_InputAxisName = axisNameX;
this.cinemachineFreeLook.m_YAxis.m_InputAxisName = axisNameY;
}
void SetCursorImage(Texture2D cursorTexture)
{
Vector2 hotspot = new Vector2(0.293f, 0.078f) * 64;
Cursor.SetCursor(cursorTexture, hotspot, CursorMode.ForceSoftware);
}
<summary>
</summary>
public enum InputModeState
{
Explore = 0,
Inventory,
Mining,
TextChat,
Max
}
InputModeState nextState = InputModeState.Inventory;
<summary>
</summary>
Dictionary<int, InputModeState> stateHashToEnum = new Dictionary<int, InputModeState>();
<summary>
</summary>
const string INPUT_STATE_LAYER_NAME = "InputStateLayer";
<summary>
</summary>
const int INPUT_STATE_LAYER_INDEX = 0;
<summary>
</summary>
Animator animator;
}
実際動かしてみると、TextChat の方、いつのまにか Canvas のモードを変えてたから位置計算がおかしくて表示されなくなっている
あんあに苦労したのに… ScreenSpace Camera なら実際の配置場所に置けるはずなので、調整してみます。
テキストバルーンの再配置
ChatBalloonLocater クラスについて覚えているだろうか
simplestar-tech.hatenablog.com
すでに6カ月ほど経過しているので、すっかり忘れてしまっていますが…
作る手順はここ
simplestar-tech.hatenablog.com
読んでわかったことは、座標系が px 単位の画面上の配置だということ
テキストに気づけるように画面内に収めようと頑張っていますが、今回もこれができるかは不明
最終的にこうなりました…
今までの苦労が消えました すっきり
using UnityEngine;
<summary>
</summary>
public class ChatBalloonLocater : MonoBehaviour
{
#region UI Connection
internal Transform balloonTransform;
#endregion
void OnGUI()
{
if (null == this.balloonTransform || !this.balloonTransform.gameObject.activeSelf)
{
return;
}
this.balloonTransform.position = this.transform.position;
}
}
現在の動き
まだ本題に入る前の準備工程ですが、いろいろと整理され始めたので、変化した現在のゲームの様子をご覧ください
ブロックを破壊するとスロットの数が増える
というか、スロットに数を表示してないので、まずはそこから行います。
slot の子供に image を配置して TextMeshPro UI をセットしてアウトラインを引いてみた
ViewSlot クラスを定義して、そこに PlayFab の ItemInstance クラスと、slotIndex, count を詰められるようにします。
これを次のようにスロット index 指定で最新状態にできるようにします。
<summary>
</summary>
<param name="slotIndex"></param>
private void UpdateSlotView(int slotIndex)
{
if (null != this.items[slotIndex])
{
this.items[slotIndex].slotIndex = slotIndex;
}
Destroy(this.slotCubes[slotIndex]);
var parent = this.slotTransforms[slotIndex];
var textMeshPro = parent.GetComponentInChildren<TextMeshProUGUI>();
if (null != textMeshPro)
{
textMeshPro.text = "";
}
var cubeObject = this.CreateSlotCubeObject(this.items[slotIndex]);
if (null != cubeObject)
{
cubeObject.transform.SetParent(parent);
cubeObject.transform.localPosition = Vector3.zero;
cubeObject.transform.localRotation = Quaternion.identity;
cubeObject.transform.localScale = new Vector3(50, 50, 1);
if (null != textMeshPro)
{
textMeshPro.text = this.items[slotIndex].count.ToString();
}
}
this.slotCubes[slotIndex] = cubeObject;
if (null != this.items[slotIndex] && null != this.items[slotIndex].item && !this.editItems.ContainsKey(this.items[slotIndex].item.ItemInstanceId))
{
this.editItems.Add(this.items[slotIndex].item.ItemInstanceId, this.items[slotIndex]);
}
}
Grid Layout で自動で敷き詰められるタイルに Button コンポーネントを追加すれば、クリックイベントが取れるようになりますので
そこに次のロジックを書けば、クリックでアイテムをホールドしつつ、その操作によってアイテムの数が見えるようになります。
<summary>
</summary>
<param name="slotIndex"></param>
private void OnClickSlot(int slotIndex)
{
var item = this.items[slotIndex];
this.items[slotIndex] = this.items[MAX_SLOT_INDEX];
this.items[MAX_SLOT_INDEX] = item;
this.UpdateSlotView(slotIndex);
this.UpdateSlotView(MAX_SLOT_INDEX);
}
ブロックを破壊するとスロットの数が増える
どうやって?
インベントリの数は増えているので、そいつの CustomData にある slot ~を見つけ出して
最も若いスロットにて 99x2 に到達していなかったら + 2 して
到達していたら、次のスロットに加算する感じ
サーバーの更新回数は極力減らしたい…
そこで、数が合わなかった時
最も若いスロットに余剰数を振り分けるのはどうか
それもダメ
毎回リクエストを投げる必要がある
クライアントから
サーバーは数が合えば ok とする
となると、破壊時に
アイテムID と重なるアイテムのインスタンスIDを取り出し、CustomData を編集する
ホットバーの作り方が見えてきた
今のアイテムの一番若い 0~8 インデックスのプロキシとして、非インベントリ時に UI として表示できればいい
そこがいっぱいになったら、アイテムボックスに入れないという処理が簡単になるアイディア
items これをいじれれば…いや、基本は次のコードでいけると思う
foreach (var item in noSlotItems)
{
List<ViewItem> viewItemList = new List<ViewItem>();
for (int slotIndex = 0; slotIndex < this.items.Length; slotIndex++)
{
if (null == this.items[slotIndex] && null != item.RemainingUses)
{
const int slotMaxPrismCount = 99 * 2;
int overSlot = Mathf.FloorToInt((int)item.RemainingUses / slotMaxPrismCount);
for (int i = overSlot; i >= 0; i--)
{
if (0 != i)
{
this.items[slotIndex] = new ViewItem { slotIndex = slotIndex, item = item, count = slotMaxPrismCount };
viewItemList.Add(this.items[slotIndex]);
for (; slotIndex < this.items.Length; slotIndex++)
{
if (null == this.items[slotIndex])
{
break;
}
}
}
else if (0 < ((int)item.RemainingUses - overSlot * slotMaxPrismCount))
{
this.items[slotIndex] = new ViewItem { slotIndex = slotIndex, item = item, count = (int)item.RemainingUses - overSlot * slotMaxPrismCount };
viewItemList.Add(this.items[slotIndex]);
this.UpdateSlotView(slotIndex);
}
}
break;
}
}
if (0 < viewItemList.Count)
{
this.UpdateUserInventoryItemCustomData(item, viewItemList);
}
}
加えて以下のロジックが原型になりそう
if (null != item.CustomData)
{
int totalSlotCount = 0;
foreach (var customData in item.CustomData)
{
var match = Regex.Match(customData.Key, "slot([0-9]{2})");
if (match.Success && 2 == match.Groups.Count && int.TryParse(customData.Value, out int itemCount))
{
int slotIndex = int.Parse(match.Groups[1].Value);
this.items[slotIndex] = new ViewItem { slotIndex = slotIndex, item = item, count = itemCount };
this.UpdateSlotView(slotIndex);
totalSlotCount += itemCount;
hasSlot = true;
}
}
そうか、すでにある CustomData の最も若い番号に加算する
そしてそれをリクエストする ですね
CustomData を持たない場合は、適当に若い空きスロットにアイテムを置く
CustomData に登録する
そしてそれをリクエストする
リクエストはいつ?
world データを変更したとき、つまり掘削時に次のように対象 Cube を Air に変更しようとします。
var chunkInt3 = this.chunkCollider.gameObject.GetComponent<ChunkMeshInfo>().chunkInt3;
this.cubeDataWorld.SetCubeData(this.cubeCenter, chunkInt3, CubeCategoryType.Basic, CubeRotationType.Top000, SideType.Air, SideType.Air);
成功するとサーバー側で Inventory の値を更新するはずなので、その成功時にインベントリを変更すると良さそう
リクエストは具体的には、次のようにして行われていた
var microWorldCubeDataByteOffset = this.GetMicroWorldCubeDataByteOffset(microWorldKey, chunkKeyXYZ, cubeIndex);
PlayFabCloudScript.RequestCubeAction(microWorldName, 1, microWorldCubeDataByteOffset, (byte)category, (byte)rotationType, (byte)sideTypeA, (byte)sideTypeB, null );
あ、あと、ユーザーからのアイテムリクエストを信じてはならない(無限増殖できてしまうので)
ということで、そのあたりを別記事に書きました。
simplestar-tech.hatenablog.com
まとめ
話題としては
ブロックの破壊と配置によるアイテムスロットの操作と
その時に行われるリクエストの反映、オーバースロット時の挙動
だったのですが、思いのほか悩んで、別記事にいくつも逃がす形で 5/7 ステップを終えました。