「 企画書と 設計書のフェーズは すっ飛ばすんでしょ。
プログラミングしかやらないから」
「 👆 Unity Hub でプロジェクトを作る所から始まるぜ」
「 👆 Unity Editor が出てくるが、自分が使いやすいように セットアップしておいたぜ。
ほんとは ディスプレイいっぱい でかく広げて作業しているが、
上の画像は ブログにアップするために ウィンドウを小さくしているぜ」
「 その ディスプレイいっぱいに広げたウィンドウの画像も 1回 見せてくれだぜ」
「 👆 トランプ・ゲーム作るんだから トランプの画像がいるだろ。
Project ウィンドウの Assets
フォルダーの下に Images
フォルダーを作って、
右クリックして Show in Explorer
をクリックしろだぜ」
「 👆 前に神経衰弱を作った時に 描いたものを フォルダーへぶち込むぜ」
「 しかし また 1枚1枚 プレーンを置いて 画像をプレーンにドラッグ&ドロップ していくのかだぜ?」
「 前に作った Concentration
(コンセントレーション;トランプの神経衰弱ゲーム) を Inport Package
(インポート・パッケージ)したらいいんじゃないの?」
📅2023-01-23 mon 21:44
「 👆 前に Import Package
したら 中身をぶちまけられたり、ぐちゃぐちゃに壊されたりしたから 嫌なんだが
まだ プロジェクトを作ったばかりだし 被害もないだろ」
「 あっ、 カードの ゲーム・オブジェクト が入ってないぜ」
「 Concentration
プロジェクトの方で カードのゲーム・オブジェクトを プレファブにして Assets に入れておけばいいんじゃないの?」
「 👆 Hierarchy ウィンドウにあるだけでは Assets ではないから、 Assets に入れないといけないのか。
下準備が けっこう居るな。
元のプロジェクトを壊さずに ゲーム・オブジェクトを プレファブに差し替えられるかな?」
「 👆 Hierarchy ウィンドウから Project ウィンドウへ ドラッグ&ドロップで コピーすることは でけるみたいだけど」
「 じゃあ Hierarchy ウィンドウにある方の Hearts 1
フォルダーを消して、
Project ウィンドウにある方の Hearts 1
プレファブを Hierarchy ウィンドウに戻してみろだぜ」
「 じゃあ 残り53枚のカードを プレファブに変換しろだぜ」
📅2023-01-23 mon 22:11
「 Hierarchy ウィンドウの方の 元のゲーム・オブジェクトも 水色のアイコンに変わってるわよ。
もう プレファブになってんじゃない?」
「 👆 なんでもいいや…… Export Package
しよ」
「 👆 Speed
プロジェクトの方で Import Package
しよ」
「 👆 プレファブだけ持ってきてもだけで 画像も持ってこないと リンク切れを起こすか
当たり前と言えば 当たり前だが」
「 トランプ・カードだけを インポート・パッケージしやすいような
トランプ・カードだけのプロジェクトを 作っておくべきなんじゃない?」」
「 下ごしらえか。 トランプ・ゲームをよく作るようなら 作っておいた方が良さそうだな」
📅2023-01-23 mon 22:36
「 👆 なんか知らんけど 裏側 剥がれてるから 貼り直しだ ひ~」
「 👆 Unity Learn のビギナーコースで学んだところによると、 Prefab
を Hierarchy
でまた いじったら、 Inspector
ウィンドウの Overrides
ドロップダウンリストから
Apply All
ボタンを選んで 押せば プレファブの設定を上書きしてくれるんだったと思う、多分」
「 じゃあ 全部のカードの Apply All
ボタンを押すのが終わったら、
Hierarchy ウィンドウのカードを全部消して、
Project ウィンドウにあるカードを Hierarchy ウィンドウへ ドラッグ&ドロップしろだぜ」
「 でけた。 今度は オモテも ウラも 画像が貼り付いてるぜ」
「 カードができたんだったら、 並べて、
スピードをやってるみたいな 画面を作りなさいよ」
「 👆 例えば 赤色のスートのカードだけ 180°回転させるとか Unity Editor を使って操作する。
こういう道具の使い方の基本操作が すばやいことが 開発屋の 基本のき だぜ」
「 👆 あっ! カードを裏返そうとしたら オモテ面と ウラ面の両方が ウラの方向いて
ワケが分からなくなった!」
「 👆 不思議な話だが プロジェクトにある カードを全部消して、
エクスポートした自分自身の中身を 再び 自分に入れ直すぜ」
「 👆 オモテ面、ウラ面の2枚で1つのカードを ひっくり返す うまい操作が よく分からん。 疲れた。
今日は ここまでだぜ」
📅2023-01-24 tue 00:05 end
「 👆 Speades 3
ゲーム・オブジェクトを選択して Rotation Z
を 180
にすれば
オモテ面、ウラ面を1つのまとまりとして 裏返してくれるぜ」
「 1個 1個 ゲーム・オブジェクトを選んで テキストボックスに 180
を入れていけだぜ」
「 この カードを1枚ずつ選んで 裏返して 少し持ち上げる たこ焼き みたいな作業をやるの
嫌なんだが」
「 あったとしても 知りようがないぜ。
ここを プログラミング化できたら 30分は 縮まる!」
📅2023-01-24 tue 21:13
「 カードの端を ちょっと被せつつ並べれば 20枚は 収まるかな?」
「 👆 真上というのも 味気ないんで ちょっと傾けつつ 整数にまとめたぜ」
「 将棋の駒だって 後手は ひっくり返ってるだろ 例はある 気にするなだぜ」
📅2023-01-24 tue 22:01
「 ゲーム開始時に セット されるプログラムを組みましょうよ」
「 xとzの正負が逆じゃないか? カメラが裏向いてんじゃないか?」
「 👆 グローバル座標にして Y軸を回転軸にして 180°回転したぜ」
📅2023-01-24 tue 22:28
「 手札は 1枚~20枚 を 位置調整することになるだろ。
予め 計算式を まとめてくれだぜ」
「 カード1枚の横幅は だいたい 10 のようだぜ。それを元に計算してみるか」
「 👆 横幅が無限にあるのなら、 (カードの枚数 - 1) * -5
の位置から +10
間隔でカードを並べるだけでいいが……」
「 👆 左端が x=-62
、右端が x=62
と決まってて、カードは20枚あるのだった」
「 62 - (-62) = 124 なので、
横幅 124 の中に 20 枚のカードがあるので、
言い換えると
横幅 124 の中に 19 箇所のカードの隙間があるので、
1つの間隔は 124 / 19 = 6.526... んー すっきりしないなあ」
左端のカードの位置 + (左から何枚目 - 1) * ((右端のカードの位置 - 左端のカードの位置) / (カードの枚数 - 1))
Example:
-62 + (左から何枚目 - 1) * (124/19)
📅2023-01-24 tue 23:02
📅2023-01-24 tue 23:25
「 👆 ゲーム・マネージャーを作る手順は 前にやったから 途中は省略するぜ」
GameManager.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
/// <summary>
/// トランプ・カード
/// </summary>
List<GameObject> goPlayingCards = new();
// Start is called before the first frame update
void Start()
{
for (int i = 1; i < 14; i++)
{
goPlayingCards.Add(GameObject.Find($"Clubs {i}"));
goPlayingCards.Add(GameObject.Find($"Diamonds {i}"));
goPlayingCards.Add(GameObject.Find($"Hearts {i}"));
goPlayingCards.Add(GameObject.Find($"Spades {i}"));
}
float posY = 0.0f;
float posYStep = -0.2f;
float posZ = 42.0f;
float posZStep = -((42.0f - (-28)) / goPlayingCards.Count);
for (int i = 0; i < goPlayingCards.Count; i++)
{
var card = goPlayingCards[i];
float x = -62.0f + i * (124 / (goPlayingCards.Count - 1));
card.transform.position = new Vector3(x, posY, posZ);
card.transform.rotation = Quaternion.Euler(0, 180, 0);
posY += posYStep;
posZ += posZStep;
}
}
// Update is called once per frame
void Update()
{
}
}
「 👆 まず、カードをかき集めて ざらっと 机に並べるコードを書いてみよう」
「 👆 カードが被ってしまって 数字が なんにも見えないが、
まあ、かき集めるのは でけたな」
📅2023-01-24 tue 23:47
📅2023-01-25 mon 19:14
「 Excel でチューリング・マシン作ってたら時間が飛んだぜ。
戻ってきたぜ」
「 カードのテクスチャーや枚数を検品しないと 不良品が混じってるかもしれないのに……」
「 じゃあ 先に進みましょう。
積みあがってる手札を 裏返しなさいよ」
📅2023-01-27 fri 21:31
「 あれっ! まだ 時計回りに180°回転してないのに ひっくり返ってるぜ!」
「 👆 なんか積んでる手札の底が1枚ずれてるな。
まあいいや 進んでる間に 原因が見つかるだろ」
「 じゃあ 先に進みましょう。
場にオープンしているカードを1枚選んで ルールを気にせず 中央の台札に 積みましょう!」
📅2023-01-27 fri 21:53
「 👆 その前に カードが 空飛んでるのが気になるぜ 地面を置こう」
「 👆 カードを持ち上げてみると……、
カメラアングルと 光源の関係なのか 手前と奥のプレイヤーで 持ち上げた高さが違って見えるぜ」
/// <summary>
/// カードを持ち上げる
/// </summary>
/// <param name="card"></param>
private void SetFocus(GameObject card)
{
var liftY = 5.0f; // 持ち上げる(パースペクティブがかかっていて、持ち上げすぎると北へ移動したように見える)
var rotateY = -5; // -5°傾ける
var rotateZ = -5; // -5°傾ける
card.transform.position = new Vector3(card.transform.position.x, card.transform.position.y + liftY, card.transform.position.z);
card.transform.rotation = Quaternion.Euler(card.transform.rotation.eulerAngles.x, card.transform.rotation.eulerAngles.y + rotateY, card.transform.eulerAngles.z + rotateZ);
}
「 行列にした方がよくないかだぜ? 逆関数 作るの めんどくさいだろ?」
「 そういう最適化は 問題点が出尽くして 完成したあとに やりたかったら やればいいんで」
/// <summary>
/// 持ち上げたカードを場に戻す
/// </summary>
/// <param name="card"></param>
private void ResetFocus(GameObject card)
{
var liftY = 5.0f; // 持ち上げる(パースペクティブがかかっていて、持ち上げすぎると北へ移動したように見える)
var rotateY = -5; // -5°傾ける
var rotateZ = -5; // -5°傾ける
// 逆をする
liftY = -liftY;
rotateY = -rotateY;
rotateZ = -rotateZ;
card.transform.position = new Vector3(card.transform.position.x, card.transform.position.y + liftY, card.transform.position.z);
card.transform.rotation = Quaternion.Euler(card.transform.rotation.eulerAngles.x, card.transform.rotation.eulerAngles.y + rotateY, card.transform.eulerAngles.z + rotateZ);
}
「 👆 逆関数をあてれば 持ち上げたカードは場に戻り、
また別のカードを持ち上げれば カードを選んでいる雰囲気が出るな」
📅2023-01-27 fri 22:40
// 1プレイヤーの1枚目のカードにフォーカスを当てる
{
if (0 < goPlayersHandCards[0].Count)
{
var goCard = goPlayersHandCards[0][0];
SetFocus(goCard);
}
}
// 1プレイヤーの1枚目のカードのフォーカスを外す
{
if (0 < goPlayersHandCards[0].Count)
{
var goCard = goPlayersHandCards[0][0];
ResetFocus(goCard);
}
}
// 1プレイヤーの1枚目のカードにフォーカスを当てる
{
if (1 < goPlayersHandCards[0].Count)
{
var goCard = goPlayersHandCards[0][1];
SetFocus(goCard);
}
}
// 2プレイヤーの1枚目のカードにフォーカスを当てる
{
if (0 < goPlayersHandCards[1].Count)
{
var goCard = goPlayersHandCards[1][0];
SetFocus(goCard);
}
}
// 2プレイヤーの1枚目のカードのフォーカスを外す
{
if (0 < goPlayersHandCards[1].Count)
{
var goCard = goPlayersHandCards[1][0];
ResetFocus(goCard);
}
}
// 2プレイヤーの2枚目のカードにフォーカスを当てる
{
if (1 < goPlayersHandCards[1].Count)
{
var goCard = goPlayersHandCards[1][1];
SetFocus(goCard);
}
}
Assets.Scripts.LazyArgs.cs
:
namespace Assets.Scripts
{
/// <summary>
/// コーディングのテクニックのための仕込み
/// </summary>
internal class LazyArgs
{
public delegate void SetValue<T>(T value);
}
}
「 👆 そこで コードを短く書けるための 仕込み をするぜ」
Assets.Scripts.GameManager.cs
:
/// <summary>
/// カードを取得
/// </summary>
/// <param name="player">何番目のプレイヤー</param>
/// <param name="cardIndex">何枚目のカード</param>
/// <param name="setCard">カードをセットする関数</param>
private void GetCard(int player, int cardIndex, LazyArgs.SetValue<GameObject> setCard)
{
if (cardIndex < goPlayersHandCards[player].Count)
{
var goCard = goPlayersHandCards[player][cardIndex];
setCard(goCard);
}
}
// 1プレイヤーの1枚目のカードにフォーカスを当てる
GetCard(0, 0, (goCard) => SetFocus(goCard));
// 1プレイヤーの1枚目のカードのフォーカスを外す
GetCard(0, 0, (goCard) => ResetFocus(goCard));
// 1プレイヤーの2枚目のカードにフォーカスを当てる
GetCard(0, 1, (goCard) => SetFocus(goCard));
// 2プレイヤーの1枚目のカードにフォーカスを当てる
GetCard(1, 0, (goCard) => SetFocus(goCard));
// 2プレイヤーの1枚目のカードのフォーカスを外す
GetCard(1, 0, (goCard) => ResetFocus(goCard));
// 2プレイヤーの2枚目のカードにフォーカスを当てる
GetCard(1, 1, (goCard) => SetFocus(goCard));
「 👆 長ったらしかったコードを ワンライナー(1行)で書けるようにしたぜ」
「 1プレイヤーが 0
で、 1枚目が 0
って分かりづらくない?」
「 序数と基数の違いだぜ 別のものなのだから仕方ない 慣れろだぜ」
📅2023-01-27 fri 23:09
// 右の台札を積み上げる
{
float x = rightCenterStackX;
float y = minY;
float z = rightCenterStackZ;
foreach (var goCard in goCenterStacksCards[0])
{
SetPosRot(goCard, x, y, z);
y += 0.2f;
}
}
「 👆 このコードだと 使い回しづらいので、使い回しやすい形にするかだぜ」
// 右の台札を積み上げる
{
float x = rightCenterStackX;
this.rightCenterStacksY = minY;
float z = rightCenterStackZ;
foreach (var goCard in goCenterStacksCards[0])
{
SetPosRot(goCard, x, this.rightCenterStacksY, z);
this.rightCenterStacksY += 0.2f;
}
}
「 👆 『台札は、場札から1枚抜いて置く』という動作に1本化しようぜ」
// 左の台札が空っぽの状態
this.leftCenterStackX = -15.0f;
this.leftCenterStackY = minY;
this.leftCenterStackZ = 10.0f;
// 左の台札を積み上げる
{
// 場札の好きなところから1枚抜いて、台札を1枚置く
var player = 1; // 2プレイヤーが
var handIndex = 0; // 場札の1枚目から
var goCard = goPlayersHandCards[player].ElementAt(handIndex); // カードを1枚抜いて
goPlayersHandCards[player].RemoveAt(handIndex);
var leftRight = 0; // 左の
goCenterStacksCards[leftRight].Add(goCard); // 台札として置く
// カードの位置と角度をセット
SetPosRot(goCard, this.leftCenterStackX, this.leftCenterStackY, this.leftCenterStackZ, angleY: 0.0f);
// 次に台札に積むカードの高さ
this.leftCenterStackY += 0.2f;
}
// 右の台札が空っぽの状態
this.rightCenterStackX = 15.0f;
this.rightCenterStackY = minY;
this.rightCenterStackZ = 0.0f;
// 右の台札を積み上げる
{
var player = 0; // 1プレイヤーが
var handIndex = 0; // 場札の1枚目から
var goCard = goPlayersHandCards[player].ElementAt(handIndex); // カードを1枚抜いて
goPlayersHandCards[player].RemoveAt(handIndex);
var leftRight = 1; // 右の
goCenterStacksCards[leftRight].Add(goCard); // 台札として置く
// カードの位置と角度をセット
SetPosRot(goCard, this.rightCenterStackX, this.rightCenterStackY, this.rightCenterStackZ);
// 次に台札に積むカードの高さ
this.rightCenterStackY += 0.2f;
}
「 👆 左の台札と、右の台札を 別にした方がいいな。
カードの移動があったときに、同時に ポリゴンの位置と角度も設定しよう」
📅2023-01-27 fri 23:50
/// <summary>
/// 場札の好きなところから1枚抜いて、台札を1枚置く
/// </summary>
/// <param name="player">何番目のプレイヤー</param>
/// <param name="handIndex">何枚目のカード</param>
/// <param name="leftRight">左なら1、右なら0</param>
private void PutCardToCenterStack(int player, int handIndex, int leftRight)
{
var goCard = goPlayersHandCards[player].ElementAt(handIndex); // カードを1枚抜いて
goPlayersHandCards[player].RemoveAt(handIndex);
goCenterStacksCards[leftRight].Add(goCard); // 台札として置く
// カードの位置をセット
SetPosRot(goCard, this.centerStacksX[leftRight], this.centerStacksY[leftRight], this.centerStacksZ[leftRight]);
// 次に台札に積むカードの高さ
this.centerStacksY[leftRight] += 0.2f;
}
// 左の台札が空っぽの状態
this.centerStacksX[1] = -15.0f;
this.centerStacksY[1] = minY;
this.centerStacksZ[1] = 10.0f;
// 右の台札が空っぽの状態
this.centerStacksX[0] = 15.0f;
this.centerStacksY[0] = minY;
this.centerStacksZ[0] = 0.0f;
// 左の台札を積み上げる
{
PutCardToCenterStack(
player: 1, // 2プレイヤーが
handIndex: 0, // 場札の1枚目から
leftRight: 0 // 左の
);
}
// 右の台札を積み上げる
{
PutCardToCenterStack(
player: 0, // 1プレイヤーが
handIndex: 0, // 場札の1枚目から
leftRight: 1 // 右の
);
}
IEnumerator DoDemo()
{
float seconds = 1.0f;
yield return new WaitForSeconds(seconds);
// 1プレイヤーの1枚目のカードにフォーカスを当てる
GetCard(0, 0, (goCard) => SetFocus(goCard));
yield return new WaitForSeconds(seconds);
// 1プレイヤーの1枚目のカードのフォーカスを外す
GetCard(0, 0, (goCard) => ResetFocus(goCard));
yield return new WaitForSeconds(seconds);
// 1プレイヤーの2枚目のカードにフォーカスを当てる
GetCard(0, 1, (goCard) => SetFocus(goCard));
yield return new WaitForSeconds(seconds);
// 1プレイヤーの2枚目のカードのフォーカスを外す
GetCard(0, 1, (goCard) => ResetFocus(goCard));
yield return new WaitForSeconds(seconds);
// 右の台札を積み上げる
{
PutCardToCenterStack(
player: 0, // 1プレイヤーが
handIndex: 1, // 場札の2枚目から
leftRight: 1 // 右の台札
);
}
yield return new WaitForSeconds(seconds);
// -
// 2プレイヤーの1枚目のカードにフォーカスを当てる
GetCard(1, 0, (goCard) => SetFocus(goCard));
yield return new WaitForSeconds(seconds);
// 2プレイヤーの1枚目のカードのフォーカスを外す
GetCard(1, 0, (goCard) => ResetFocus(goCard));
yield return new WaitForSeconds(seconds);
// 2プレイヤーの2枚目のカードにフォーカスを当てる
GetCard(1, 1, (goCard) => SetFocus(goCard));
yield return new WaitForSeconds(seconds);
}
「 次は、手札から 1枚取ってきて 場札として置く動作を作りなさいよ。
そのとき 場札の位置が 歯抜けだったりするだろうから、位置の再調整がいるかもしれないわね」
📅2023-01-28 fri 00:20 end
「 👇 場札を並べるコードは 以下のように書いているんだが……」
Assets.Scripts.GameManager.cs
:
// 2プレイヤーの場札を並べる(画面では、左から右へ並べる)
{
float x = maxX;
float y = minY;
float z = player2HandZ;
float xStep = (maxX - minX) / (goPlayersHandCards[1].Count - 1);
foreach (var goCard in goPlayersHandCards[1])
{
SetPosRot(goCard, x, y, z, angleY: 0.0f);
x -= xStep;
}
}
// 中略
// 1プレイヤーの場札を並べる(画面では、右から左へ並べる)
{
float x = minX;
float y = minY;
float z = player1HandZ;
float xStep = (maxX - minX) / (goPlayersHandCards[0].Count - 1);
foreach (var goCard in goPlayersHandCards[0])
{
SetPosRot(goCard, x, y, z);
x += xStep;
}
}
「 👆 1プレイヤーが 右から左へ、 2プレイヤーが 左から右へ、 みたいな 方向がまったく逆のものを
1本化 するのは ちょっと すっきりしない方法を使うぜ」
public class GameManager : MonoBehaviour
{
/// <summary>
/// 西端
/// </summary>
readonly float minX = -62.0f;
/// <summary>
/// 東端
/// </summary>
readonly float maxX = 62.0f;
/// <summary>
/// 底端
///
/// - `0.0f` は盤
/// </summary>
readonly float minY = 0.5f;
readonly float player2HandZ = 42.0f;
readonly float player2PileZ = 26.0f;
readonly float player1PileZ = -12.0f;
readonly float player1HandZ = -28.0f;
「 👆 その前に 決まりきった座標を 読取専用でメソッドからアクセスできる自由変数にしておこうぜ」
/// <summary>
/// 場札を並べる
/// </summary>
void ArrangeHandCardsP2(int player)
{
// 2プレイヤーの場札を並べる(画面では、左から右へ並べる)
float x = maxX;
float xStep = (maxX - minX) / (goPlayersHandCards[player].Count - 1);
foreach (var goCard in goPlayersHandCards[player])
{
SetPosRot(goCard, x, minY, player2HandZ, angleY: 0.0f);
x -= xStep;
}
}
/// <summary>
/// 場札を並べる
/// </summary>
void ArrangeHandCardsP1(int player)
{
// 1プレイヤーの場札を並べる(画面では、右から左へ並べる)
float x = minX;
float xStep = (maxX - minX) / (goPlayersHandCards[player].Count - 1);
foreach (var goCard in goPlayersHandCards[player])
{
SetPosRot(goCard, x, minY, player1HandZ);
x += xStep;
}
}
「 👆 プログラマーのやってることって、違う書き方のコードを1本化することだよな。
この2つを 1本化しようぜ?」
「 👆 プレイヤー1と2で 違う変数を使っているのを止めて、配列かリストにしようぜ?」
readonly float[] handCardsZ = new[] { -28.0f, 42.0f };
readonly float[] pileCardsZ = new[] { -12.0f, 26.0f };
/// <summary>
/// 場札を並べる
/// </summary>
void ArrangeHandCards(int player)
{
float angleY;
float stepSign;
float x;
switch (player)
{
case 0:
// 1プレイヤーの場札は、画面では、右から左へ並べる
angleY = 180.0f;
stepSign = 1;
x = minX;
break;
case 1:
// 2プレイヤーの場札は、画面では、左から右へ並べる
angleY = 0.0f;
stepSign = -1;
x = maxX;
break;
default:
throw new Exception();
}
float xStep = stepSign * (maxX - minX) / (goPlayersHandCards[player].Count - 1);
foreach (var goCard in goPlayersHandCards[player])
{
SetPosRot(goCard, x, minY, handCardsZ[player], angleY: angleY);
x += xStep;
}
}
📅2023-01-28 sat 18:46
「 👆 次は 『手札から1枚抜いて場札に置く』というモーションを1つの関数にしたいぜ」
/// <summary>
/// 手札からn枚抜いて、場札へ移動する
///
/// - 場札は並び直される
/// </summary>
void AddCardsToHandFromPile(int player, int numberOfCards)
{
// 手札からn枚抜いて、場札へ移動する
var goCards = goPlayersPileCards[player].GetRange(0, numberOfCards);
goPlayersPileCards[player].RemoveRange(0, numberOfCards);
goPlayersHandCards[player].AddRange(goCards);
// 場札を並べる
ArrangeHandCards(player);
}
📅2023-01-28 sat 19:12
「 手札を先頭から抜いたら、後ろのカードが浮いたままになってしまうわよ?」
/// <summary>
/// 手札の上の方からn枚抜いて、場札の後ろへ追加する
///
/// - 画面上の場札は位置調整される
/// </summary>
void AddCardsToHandFromPile(int player, int numberOfCards)
{
// 手札の上の方からn枚抜いて、場札へ移動する
var length = goPlayersPileCards[player].Count; // 手札の枚数
if (numberOfCards <= length)
{
var startIndex = length - numberOfCards;
var goCards = goPlayersPileCards[player].GetRange(startIndex, numberOfCards);
goPlayersPileCards[player].RemoveRange(startIndex, numberOfCards);
goPlayersHandCards[player].AddRange(goCards);
// 場札を並べる
ArrangeHandCards(player);
}
}
📅2023-01-28 sat 19:51
「 👆 スピードをやってて、手札を積み上げるのって 全部で どういうケースがあるの?」
「 初回で カードを配るときじゃないか?
ゲームが始まったら 手札にカードを積む という動きは無いだろ」
「 どこからでもない。
Unity のシーン上に ゲーム・オブジェクトが適当に散らばっているぜ」
「 関数にしたいのよ。
『どこかに置いてあって、それを手札に積む』という定型パターンに乗せたいから、
どこかに置いてあることにしなさいよ」
「 手札以外には、台札と 場札しかないぜ。
そのどっちかだろ」
「 じゃあ ゲーム開始時に 散らばっているカードは
台札という扱いにして、ゲーム開始時に 『台札を色分けして、手札に積む』という定型パターンに乗せなさいよ」
// 台札
float[] centerStacksX = { 15.0f, -15.0f };
/// <summary>
/// 台札のY座標
///
/// - 右が 0、左が 1
/// - 0.0f は盤なので、それより上にある
/// </summary>
float[] centerStacksY = { 0.5f, 0.5f };
float[] centerStacksZ = { 0.0f, 10.0f };
「 👆 台札の一番上のカードのY座標を 外側に追いやって……」
/// <summary>
/// 台札を、手札へ移動する
/// </summary>
/// <param name="rightLeft">右:0, 左:1</param>
void AddCardsToPileFromCenterStacks(int rightLeft)
{
// 台札の一番上(一番後ろ)のカードを1枚抜く
var numberOfCards = 1;
var length = goCenterStacksCards[rightLeft].Count; // 手札の枚数
if (1 <= length)
{
var startIndex = length - numberOfCards;
var goCard = goCenterStacksCards[rightLeft].ElementAt(startIndex);
goCenterStacksCards[rightLeft].RemoveAt(startIndex);
// 黒いカードは1プレイヤー、赤いカードは2プレイヤー
int player;
float angleY;
if (goCard.name.StartsWith("Clubs") || goCard.name.StartsWith("Spades"))
{
player = 0;
angleY = 180.0f;
}
else if (goCard.name.StartsWith("Diamonds") || goCard.name.StartsWith("Hearts"))
{
player = 1;
angleY = 0.0f;
}
else
{
throw new Exception();
}
// プレイヤーの手札を積み上げる
goPlayersPileCards[player].Add(goCard);
SetPosRot(goCard, pileCardsX[player], pileCardsY[player], pileCardsZ[player], angleY: angleY, angleZ: 180.0f);
pileCardsY[player] += 0.2f;
}
}
void Start()
{
// ゲーム開始時、とりあえず、すべてのカードは、いったん右の台札という扱いにする
for (int i = 1; i < 14; i++)
{
// 右の台札
goCenterStacksCards[0].Add(GameObject.Find($"Clubs {i}"));
goCenterStacksCards[0].Add(GameObject.Find($"Diamonds {i}"));
goCenterStacksCards[0].Add(GameObject.Find($"Hearts {i}"));
goCenterStacksCards[0].Add(GameObject.Find($"Spades {i}"));
}
// 右の台札をシャッフル
var rightLeft = 0;// 右
goCenterStacksCards[rightLeft] = goCenterStacksCards[rightLeft].OrderBy(i => Guid.NewGuid()).ToList();
// 右の台札をすべて、色分けして、黒色なら1プレイヤーの、赤色なら2プレイヤーの、手札に乗せる
while (0 < goCenterStacksCards[rightLeft].Count)
{
AddCardsToPileFromCenterStacks(rightLeft);
}
📅2023-01-28 sat 21:17
// 2プレイヤーが、場札の1枚目を抜いて、左の台札へ積み上げる
PutCardToCenterStack(
player: 1, // 2プレイヤーが
handIndex: 0, // 場札の1枚目から
rightLeft: 0 // 左の
);
// 2プレイヤーの場札の位置調整
ArrangeHandCards(1);
// 1プレイヤーが、場札の1枚目を抜いて、右の台札へ積み上げる
PutCardToCenterStack(
player: 0, // 1プレイヤーが
handIndex: 0, // 場札の1枚目から
rightLeft: 1 // 右の
);
// 1プレイヤーの場札の位置調整
ArrangeHandCards(0);
「 👆 場札の位置調整を 毎回書くのも煩わしいから 関数の中に入れるかだぜ」
/// <summary>
/// 場札の好きなところから1枚抜いて、台札を1枚置く
/// </summary>
/// <param name="player">何番目のプレイヤー</param>
/// <param name="handIndex">何枚目のカード</param>
/// <param name="rightLeft">右なら0、左なら1</param>
private void PutCardToCenterStackFromHand(int player, int handIndex, int rightLeft)
{
var goCard = goPlayersHandCards[player].ElementAt(handIndex); // カードを1枚抜いて
goPlayersHandCards[player].RemoveAt(handIndex);
goCenterStacksCards[rightLeft].Add(goCard); // 台札として置く
// カードの位置をセット
SetPosRot(goCard, this.centerStacksX[rightLeft], this.centerStacksY[rightLeft], this.centerStacksZ[rightLeft]);
// 次に台札に積むカードの高さ
this.centerStacksY[rightLeft] += 0.2f;
// 場札の位置調整
ArrangeHandCards(player);
}
「 👆 モデルへの編集と、画面への編集は 同じ関数に入れない方がいいんだが、
それは あとで考えるぜ。
関数名も変更」
void Start()
{
// ゲーム開始時、とりあえず、すべてのカードは、いったん右の台札という扱いにする
const int right = 0;// 台札の右
const int left = 1;// 台札の左
for (int i = 1; i < 14; i++)
{
// 右の台札
goCenterStacksCards[right].Add(GameObject.Find($"Clubs {i}"));
goCenterStacksCards[right].Add(GameObject.Find($"Diamonds {i}"));
goCenterStacksCards[right].Add(GameObject.Find($"Hearts {i}"));
goCenterStacksCards[right].Add(GameObject.Find($"Spades {i}"));
}
// 右の台札をシャッフル
goCenterStacksCards[right] = goCenterStacksCards[right].OrderBy(i => Guid.NewGuid()).ToList();
// 右の台札をすべて、色分けして、黒色なら1プレイヤーの、赤色なら2プレイヤーの、手札に乗せる
while (0 < goCenterStacksCards[right].Count)
{
AddCardsToPileFromCenterStacks(right);
}
// 1,2プレイヤーについて、手札から5枚抜いて、場札として置く(画面上の場札の位置は調整される)
AddCardsToHandFromPile(player: 0, numberOfCards: 5);
AddCardsToHandFromPile(player: 1, numberOfCards: 5);
// 2プレイヤーが、場札の1枚目を抜いて、左の台札へ積み上げる
PutCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
handIndex: 0, // 場札の1枚目から
place: left // 左の
);
// 1プレイヤーが、場札の1枚目を抜いて、右の台札へ積み上げる
PutCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
handIndex: 0, // 場札の1枚目から
place: right // 右の
);
StartCoroutine("DoDemo");
}
📅2023-01-28 sat 21:37
「 👆 場札の間隔が空きすぎていて、スピードをしてる感じ、しなくない?」
「 👆 こういう感じか。 まるで プロポーショナル・フォント みたいだな」
「 👆 カードの境界線が見えないから 1枚の長い紙みたいに見えるわよ」
📅2023-01-28 sat 22:09
/// <summary>
/// 場札を並べる
/// </summary>
void ArrangeHandCards(int player)
{
int numberOfCards = goPlayersHandCards[player].Count; // カードの枚数
if (numberOfCards < 1)
{
return;
}
float cardAngleZ = -5; // カードの少しの傾き
float cardWidth = 10; // カードの横幅
float marginRight = -2; // カードは隣のカードと少し重なる
float wholeWidth = numberOfCards * cardWidth + ((numberOfCards - 1) * marginRight); // 場札全体の横幅
float centerOfLeftestCard = -(wholeWidth / 2 - (cardWidth / 2)); // 1プレイヤーから見て一番左のカードの中心座標
float angleY;
float stepSign;
float x;
switch (player)
{
case 0:
// 1プレイヤーの場札は、画面では、右から左へ並べる
angleY = 180.0f;
stepSign = 1;
x = centerOfLeftestCard;
break;
case 1:
// 2プレイヤーの場札は、画面では、左から右へ並べる
angleY = 0.0f;
stepSign = -1;
x = -centerOfLeftestCard;
break;
default:
throw new Exception();
}
float xStep = stepSign * (cardWidth + marginRight);
foreach (var goCard in goPlayersHandCards[player])
{
SetPosRot(goCard, x, minY, handCardsZ[player], angleY: angleY, angleZ: cardAngleZ);
x += xStep;
}
}
「 👆 2プレイヤー側は いい感じに影が付いたが、1プレイヤー側は 光の当たり方のせいで 思ったようにはなってないぜ」
📅2023-01-28 sat 22:17
「 👆 頭の運動不足の頭では n枚のとき角度は何mがいいのか ぱっと出てこないが まあ 手調整してみるか」
📅2023-01-28 sat 22:58
「 👆 うーん、 どうすればいいのか。ちょっと 考えようか」
「 何発で決まったとか ユーザーには分かんないから 目視確認と 手調整を繰り返せばいいのよ」
「 半径が 100、スタートの角度が 110°、 間隔の角度は -4° でこれだから、
もっと半径を大きくして カーブを緩くするか」
「 👆 半径が 200、スタートの角度が 112°、 間隔の角度は -1.83°、 円の中心のz位置を +10」
「 うまく画面に収めたが、ゲーム中にこんなケースは出てこないのでは?」
「 盤の周りが スカスカに空いてる分には ユーザーも困らないでしょう」
「 台札は カードが積み重なっていくはずだから、 画面の下側に 気持ち ずらした方がよくない?」
📅2023-01-28 sat 22:58
「 👆 几帳面に真上に積むから 2Pのカードと被って見えることは無さそうだぜ」
「 👆 ランダムにずれを入れると どこに行くか わからんけど」
「 台札の所定の位置から離れていくのが おかしいのと、
カードが几帳面に 正方形の角度が ぶれてないのが おかしいんだぜ」
「 1プレイヤー、2プレイヤーが 右利きか、左利きかでも 変わってくるんじゃないかだぜ?」
「 そんなん 設定するのも 嬉しさがあるのか分からないので 右利き ということにしとこうぜ?」
「 👆 少しは揃えてみたのと、1プレイヤーと 2プレイヤーで 大きく捻る回転方向を ずらしたぜ」
/// <summary>
/// ぴったり積むと不自然だから、X と Z を少しずらすための仕組み
///
/// - 1プレイヤー、2プレイヤーのどちらも右利きと仮定
/// </summary>
/// <param name="player"></param>
/// <returns></returns>
(float, float, float) MakeShakeForCenterStack(int player)
{
// 1プレイヤーから見て。左上にずれていくだろう
var left = -1.5f;
var right = 0.5f;
var bottom = -0.5f;
var top = 1.5f;
var angleY = UnityEngine.Random.Range(-10, 40); // 反時計回りに大きく捻りそう
switch (player)
{
case 0:
return (UnityEngine.Random.Range(left, right), UnityEngine.Random.Range(bottom, top), angleY);
case 1:
return (UnityEngine.Random.Range(-right, -left), UnityEngine.Random.Range(-top, -bottom), angleY);
default:
throw new Exception();
}
}
📅2023-01-29 sat 00:42
「 👆 カードのセンタリングが まだ作ってないので 作るぜ」
ソースコード:
/// <summary>
/// 場札を並べる
/// </summary>
void ArrangeHandCards(int player)
{
// 25枚の場札が並べるように調整してある
int numberOfCards = goPlayersHandCards[player].Count; // カードの枚数
if (numberOfCards < 1)
{
return;
}
float cardAngleZ = -5; // カードの少しの傾き
int range = 200; // 半径。大きな円にするので、中心を遠くに離したい
int offsetCircleCenterZ; // 中心位置の調整
float angleY;
float playerTheta;
// float leftestAngle = 112.0f;
float angleStep = -1.83f;
float startTheta = (numberOfCards * Mathf.Abs(angleStep) / 2 - Mathf.Abs(angleStep) / 2 + 90.0f) * Mathf.Deg2Rad;
float thetaStep = angleStep * Mathf.Deg2Rad; ; // 時計回り
float ox = 0.0f;
float oz = handCardsZ[player];
switch (player)
{
case 0:
// 1プレイヤー
angleY = 180.0f;
playerTheta = 0;
offsetCircleCenterZ = -190;
break;
case 1:
// 2プレイヤー
angleY = 0.0f;
playerTheta = 180 * Mathf.Deg2Rad;
offsetCircleCenterZ = 188; // カメラのパースペクティブが付いているから、目視で調整
break;
default:
throw new Exception();
}
float theta = startTheta;
foreach (var goCard in goPlayersHandCards[player])
{
float x = range * Mathf.Cos(theta + playerTheta) + ox;
float z = range * Mathf.Sin(theta + playerTheta) + oz + offsetCircleCenterZ;
SetPosRot(goCard, x, minY, z, angleY: angleY, angleZ: cardAngleZ);
theta += thetaStep;
}
}
📅2023-01-29 sat 01:04
「 カーソル・キーの 左、右で ピックアップしている場札を 隣の札に変えるようにしましょう」
「 すると、今どの場札に フォーカスが当たっているかを 変数として持ちたいよな」
📅2023-01-29 sat 01:23
Assets.Scripts.GameManager.cs
:
void Update()
{
// 1プレイヤー
if (Input.GetKey(KeyCode.UpArrow))
{
// TODO 選択中の場札を1枚抜いて、左の台札に置く
}
else if (Input.GetKey(KeyCode.DownArrow))
{
// TODO 選択中の場札を1枚抜いて、右の台札に置く
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
// TODO 左隣の場札を選択する
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
// TODO 右隣の場札を選択する
}
// 2プレイヤー
if (Input.GetKey(KeyCode.W))
{
// TODO (1プレイヤー視点で言うと)選択中の場札を1枚抜いて、右の台札に置く
}
else if (Input.GetKey(KeyCode.S))
{
// TODO (1プレイヤー視点で言うと)選択中の場札を1枚抜いて、左の台札に置く
}
else if (Input.GetKeyDown(KeyCode.A))
{
// TODO (1プレイヤー視点で言うと)右隣の場札を選択する
}
else if (Input.GetKeyDown(KeyCode.D))
{
// TODO (1プレイヤー視点で言うと)右隣の場札を選択する
}
}
「 だいたい こんなもんでしょう。
あとで 気づいたら そのとき 追加しましょう」
📅2023-01-29 sat 15:24
Assets.Scripts.GameManager.cs
:
/// <summary>
/// プレイヤーが選択しているカードは、先頭から何枚目
/// </summary>
int[] playsersFocusedCardIndex ={ 0, 0 };
「 👆 何枚目のカードを選択しているか、覚えさせることにするぜ」
/// <summary>
/// 左(前側)のカードをフォーカスします
/// </summary>
/// <param name="player"></param>
void MoveFocusToLeftCard(int player)
{
var previous = playsersFocusedCardIndex[player];
var current = previous - 1;
if (current < 1)
{
return;
}
// 前にフォーカスしていたカードを、盤に下ろす
var goPreviousCard = goPlayersHandCards[player][previous];
ResetFocusHand(goPreviousCard);
// 今回フォーカスするカードを持ち上げる
var goCurrentCard = goPlayersHandCards[player][current];
SetFocusHand(goCurrentCard);
}
/// <summary>
/// 右(後ろ側)のカードをフォーカスします
/// </summary>
/// <param name="player"></param>
void MoveFocusToRightCard(int player)
{
var previous = playsersFocusedCardIndex[player];
var current = previous + 1;
if (goPlayersHandCards[player].Count <= current)
{
return;
}
// 前にフォーカスしていたカードを、盤に下ろす
var goPreviousCard = goPlayersHandCards[player][previous];
ResetFocusHand(goPreviousCard);
// 今回フォーカスするカードを持ち上げる
var goCurrentCard = goPlayersHandCards[player][current];
SetFocusHand(goCurrentCard);
}
📅2023-01-29 sat 15:42
/// <summary>
/// 隣のカードへフォーカスを移します
/// </summary>
/// <param name="player"></param>
/// <param name="direction">後ろ:0, 前:1</param>
void MoveFocusToNextCard(int player, int direction)
{
int previous;
int current;
switch (direction)
{
case 0:
previous = playsersFocusedCardIndex[player];
current = previous + 1;
if (goPlayersHandCards[player].Count <= current)
{
return;
}
break;
case 1:
previous = playsersFocusedCardIndex[player];
current = previous - 1;
if (current < 0)
{
return;
}
break;
default:
throw new Exception();
}
// 前にフォーカスしていたカードを、盤に下ろす
var goPreviousCard = goPlayersHandCards[player][previous];
ResetFocusHand(goPreviousCard);
// 今回フォーカスするカードを持ち上げる
var goCurrentCard = goPlayersHandCards[player][current];
SetFocusHand(goCurrentCard);
// 更新
playsersFocusedCardIndex[player] = current;
}
📅2023-01-29 sat 15:45
古いコード:
// 1プレイヤーの1枚目のカードにフォーカスを当てる
GetCard(0, 0, (goCard) => SetFocusHand(goCard));
yield return new WaitForSeconds(seconds);
// 1プレイヤーの1枚目のカードのフォーカスを外す
GetCard(0, 0, (goCard) => ResetFocusHand(goCard));
yield return new WaitForSeconds(seconds);
// 1プレイヤーの2枚目のカードにフォーカスを当てる
GetCard(0, 1, (goCard) => SetFocusHand(goCard));
yield return new WaitForSeconds(seconds);
// 1プレイヤーの2枚目のカードのフォーカスを外す
GetCard(0, 1, (goCard) => ResetFocusHand(goCard));
yield return new WaitForSeconds(seconds);
新しいコード:
// 1プレイヤーの1枚目のカードにフォーカスを当てる
GetCard(0, 0, (goCard) => SetFocusHand(goCard));
yield return new WaitForSeconds(seconds);
// 1プレイヤーの右隣のカードへフォーカスを移します
MoveFocusToNextCard(0, 0);
yield return new WaitForSeconds(seconds);
// 1プレイヤーの2枚目のカードのフォーカスを外す
GetCard(0, 1, (goCard) => ResetFocusHand(goCard));
yield return new WaitForSeconds(seconds);
「 『フォーカスされているカードはない』という状態を 有りにすれば、
『1枚目のカードにフォーカスを当てる』のも、
『1プレイヤーの右隣のカードへフォーカスを移します』で代用できるんじゃないの?」
/// <summary>
/// プレイヤーが選択している場札は、先頭から何枚目
///
/// - 選択中の場札が無いなら、-1
/// </summary>
int[] playsersFocusedCardIndex = { -1, -1 };
/// <summary>
/// 隣のカードへフォーカスを移します
/// </summary>
/// <param name="player"></param>
/// <param name="direction">後ろ:0, 前:1</param>
void MoveFocusToNextCard(int player, int direction)
{
int previous;
int current;
switch (direction)
{
case 0:
var length = goPlayersHandCards[player].Count;
previous = playsersFocusedCardIndex[player];
if (previous==-1)
{
// 最後尾の外から、最後尾へ入ってくる
current = length - 1;
}
else
{
current = previous + 1;
}
if (length <= current)
{
return;
}
break;
case 1:
previous = playsersFocusedCardIndex[player];
if (previous==-1)
{
// 先頭の外から、先頭へ入ってくる
current = 0;
}
else
{
current = previous - 1;
}
if (current < 0)
{
return;
}
break;
default:
throw new Exception();
}
// 前にフォーカスしていたカードを、盤に下ろす
var goPreviousCard = goPlayersHandCards[player][previous];
ResetFocusHand(goPreviousCard);
// 今回フォーカスするカードを持ち上げる
var goCurrentCard = goPlayersHandCards[player][current];
SetFocusHand(goCurrentCard);
// 更新
playsersFocusedCardIndex[player] = current;
}
for (int i=0; i<2; i++)
{
// 1プレイヤーの右隣のカードへフォーカスを移します
MoveFocusToNextCard(0, 0);
yield return new WaitForSeconds(seconds);
}
// 1プレイヤーの2枚目のカードのフォーカスを外す
GetCard(0, 1, (goCard) => ResetFocusHand(goCard));
yield return new WaitForSeconds(seconds);
📅2023-01-29 sat 16:03
「 持ってる場札を、台札の上に置いたら、
場札は どれをピックアップしている状態に戻るんだぜ?」
「 じゃあ 抜いたカードの右隣を ピックアップするようにしたらいいんじゃないの?」
「 一番右端のカードを ピックアップしたらいいのよ。
何もピックアップしないというのも 自然だけど」
📅2023-01-29 sat 16:21
void Start()
{ // ... 略
// 2プレイヤーが、場札の1枚目を抜いて、左の台札へ積み上げる
MoveCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
handIndex: 0, // 場札の1枚目から
place: left // 左の
);
// 1プレイヤーが、場札の1枚目を抜いて、右の台札へ積み上げる
MoveCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
handIndex: 0, // 場札の1枚目から
place: right // 右の
);
StartCoroutine("DoDemo");
}
「 👆 既存のコードでは、任意の場所のカードを引き抜けたが、
これを ピックアップしているカードを引き抜く ように固定したいぜ」
private void MoveCardToCenterStackFromHand(int player, int handIndex, int place)
{
var goCard = goPlayersHandCards[player].ElementAt(handIndex); // カードを1枚抜いて
goPlayersHandCards[player].RemoveAt(handIndex);
「 👆 つまり handIndex 変数を廃止して、
playsersFocusedCardIndex[player]
を参照するように 1本化したいぜ」
private void MoveCardToCenterStackFromHand(int player, int place)
{
int handIndex = playsersFocusedCardIndex[player]; // 何枚目の場札をピックアップしているか
if (handIndex < 0 || goPlayersHandCards[player].Count <= handIndex) // 範囲外は無視
{
return;
}
var goCard = goPlayersHandCards[player].ElementAt(handIndex); // カードを1枚抜いて
goPlayersHandCards[player].RemoveAt(handIndex);
if (goPlayersHandCards[player].Count <= handIndex) // 範囲外アクセス防止対応
{
handIndex = goPlayersHandCards[player].Count - 1;
}
// ... 場札の位置調整が済んだ後で
if (0 <= handIndex && handIndex < goPlayersHandCards[player].Count) // 範囲内なら
{
// 抜いたカードの右隣のカードを(有れば)ピックアップする
var goNewPickupCard = goPlayersHandCards[player].ElementAt(handIndex);
SetFocusHand(goNewPickupCard);
}
void Start()
{ // ... 略
// 1プレイヤーの先頭のカードへフォーカスを移します
MoveFocusToNextCard(player: 0, direction: 0);
// 2プレイヤーの先頭のカードへフォーカスを移します
MoveFocusToNextCard(player: 1, direction: 0);
// 2プレイヤーが、ピックアップ中の場札を抜いて、左の台札へ積み上げる
MoveCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
place: left // 左の
);
// 1プレイヤーが、ピックアップ中の場札を抜いて、右の台札へ積み上げる
MoveCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
place: right // 右の
);
StartCoroutine("DoDemo");
}
📅2023-01-29 sat 16:44
「 場札を並べ直すと、場札を全部 盤の上に置いてしまって ピックアップを忘れているぜ」
/// <summary>
/// 場札を並べなおすと、持ち上げていたカードを下ろしてしまうので、再度、持ち上げる
/// </summary>
void ResumeCardPickup(int player)
{
int handIndex = playsersFocusedCardIndex[player]; // 何枚目の場札をピックアップしているか
if (0 <= handIndex && handIndex < goPlayersHandCards[player].Count) // 範囲内なら
{
// 抜いたカードの右隣のカードを(有れば)ピックアップする
var goNewPickupCard = goPlayersHandCards[player].ElementAt(handIndex);
SetFocusHand(goNewPickupCard);
}
}
/// <summary>
/// 場札を並べる
/// </summary>
void ArrangeHandCards(int player)
{ // ... 略
// 場札を並べなおすと、持ち上げていたカードを下ろしてしまうので、再度、持ち上げる
ResumeCardPickup(player);
}
「 👆 場札の並べなおしメソッドに組み込んでしまおう。
他の箇所の既存の 持ち上げ直しコードは消すぜ」
📅2023-01-29 sat 17:07
void Update()
{
// 1プレイヤー
if (Input.GetKeyDown(KeyCode.UpArrow))
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)左の台札へ積み上げる
MoveCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
place: left // 左の
);
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
MoveCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
place: right // 右の
);
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
// 1プレイヤーのピックアップしているカードから見て、(1プレイヤーから見て)左隣のカードをピックアップするように変えます
MoveFocusToNextCard(0, 1);
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
// 1プレイヤーのピックアップしているカードから見て、(1プレイヤーから見て)右隣のカードをピックアップするように変えます
MoveFocusToNextCard(0, 0);
}
// 2プレイヤー
if (Input.GetKeyDown(KeyCode.W))
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
MoveCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
place: right // 右の
);
}
else if (Input.GetKeyDown(KeyCode.S))
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)左の台札へ積み上げる
MoveCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
place: left // 左の
);
}
else if (Input.GetKeyDown(KeyCode.A))
{
// 2プレイヤーのピックアップしているカードから見て、(2プレイヤーから見て)左隣のカードをピックアップするように変えます
MoveFocusToNextCard(1, 1);
}
else if (Input.GetKeyDown(KeyCode.D))
{
// 2プレイヤーのピックアップしているカードから見て、(2プレイヤーから見て)右隣のカードをピックアップするように変えます
MoveFocusToNextCard(1, 0);
}
}
📅2023-01-29 sat 17:18
「 動かしてみると 配列の範囲を超えるエラーと よく出会うので リミットチェックを入れていくぜ」
/// <summary>
/// 隣のカードへフォーカスを移します
/// </summary>
/// <param name="player"></param>
/// <param name="direction">後ろ:0, 前:1</param>
void MoveFocusToNextCard(int player, int direction)
{
int previous;
int current;
var length = goPlayersHandCards[player].Count;
switch (direction)
{
// 後ろへ
case 0:
previous = playsersFocusedCardIndex[player];
if (previous == -1)
{
// (ピックアップしているカードが無いとき)先頭の外から、先頭へ入ってくる
current = 0;
}
else
{
current = previous + 1;
if (length <= current)
{
// 範囲外は -1 ということにしておく
current = -1;
}
}
break;
// 前へ
case 1:
previous = playsersFocusedCardIndex[player];
if (previous == -1)
{
// (ピックアップしているカードが無いとき)最後尾の外から、最後尾へ入ってくる
current = length - 1;
}
else
{
current = previous - 1;
// - 1 になるケースもある
}
break;
default:
throw new Exception();
}
// 更新
playsersFocusedCardIndex[player] = current;
if (0 <= previous && previous < goPlayersHandCards[player].Count) // 範囲内なら
{
// 前にフォーカスしていたカードを、盤に下ろす
var goPreviousCard = goPlayersHandCards[player][previous];
ResetFocusHand(goPreviousCard);
}
if (0 <= current && current < goPlayersHandCards[player].Count) // 範囲内なら
{
// 今回フォーカスするカードを持ち上げる
var goCurrentCard = goPlayersHandCards[player][current];
SetFocusHand(goCurrentCard);
}
}
📅2023-01-29 sat 17:40
「 右端の場札を 台札の上に置いたら、
何も場札を選択していない状態になるんだけど、
この瞬間、何ボタンを押したらいいのか 分かんなくなっちゃうのよね」
「 じゃあ、場札がある限り、必ず なんらかの場札が1枚 ピックアップされている状態にした方が いいのかだぜ?」
/// <summary>
/// 隣のカードへフォーカスを移します
/// </summary>
/// <param name="player"></param>
/// <param name="direction">後ろ:0, 前:1</param>
void MoveFocusToNextCard(int player, int direction)
{
int previous = playsersFocusedCardIndex[player];
int current;
var length = goPlayersHandCards[player].Count;
if (length < 1)
{
// 場札が無いなら、何もピックアップされていません
current = -1;
}
else
{
switch (direction)
{
// 後ろへ
case 0:
if (previous == -1 || length <= previous + 1)
{
// (ピックアップしているカードが無いとき)先頭の外から、先頭へ入ってくる
current = 0;
}
else
{
current = previous + 1;
}
break;
// 前へ
case 1:
if (previous == -1 || previous - 1 < 0)
{
// (ピックアップしているカードが無いとき)最後尾の外から、最後尾へ入ってくる
current = length - 1;
}
else
{
current = previous - 1;
}
break;
default:
throw new Exception();
}
}
// 更新
playsersFocusedCardIndex[player] = current;
if (0 <= previous && previous < goPlayersHandCards[player].Count) // 範囲内なら
{
// 前にフォーカスしていたカードを、盤に下ろす
var goPreviousCard = goPlayersHandCards[player][previous];
ResetFocusHand(goPreviousCard);
}
if (0 <= current && current < goPlayersHandCards[player].Count) // 範囲内なら
{
// 今回フォーカスするカードを持ち上げる
var goCurrentCard = goPlayersHandCards[player][current];
SetFocusHand(goCurrentCard);
}
}
「 👆 左右への移動は 右端が左端とつながっているようにループさせて」
/// <summary>
/// 場札の好きなところから1枚抜いて、台札を1枚置く
/// </summary>
/// <param name="player">何番目のプレイヤー</param>
/// <param name="place">右なら0、左なら1</param>
private void MoveCardToCenterStackFromHand(int player, int place)
{
int handIndex = playsersFocusedCardIndex[player]; // 何枚目の場札をピックアップしているか
if (handIndex < 0 || goPlayersHandCards[player].Count <= handIndex) // 範囲外は無視
{
return;
}
var goCard = goPlayersHandCards[player].ElementAt(handIndex); // カードを1枚抜いて
goPlayersHandCards[player].RemoveAt(handIndex);
if (handIndex < 0 && 0 < goPlayersHandCards[player].Count)
{
handIndex = 0;
}
else if (goPlayersHandCards[player].Count <= handIndex) // 範囲外アクセス防止対応
{
// 一旦、最後尾へ
handIndex = goPlayersHandCards[player].Count - 1;
}
// それでも範囲外なら、負の数
playsersFocusedCardIndex[player] = handIndex; // 更新:何枚目の場札をピックアップしているか
// ... 略
「 👆 台札の移動は 場札のどれかを必ず選択しておくようにするぜ」
📅2023-01-29 sat 18:03
「 腰が痛くて 目が覚めたので 朝から練習しよ。
えーと どこまで やったっけ?」
「 👆 フーン。
何やってたかな。手札を、場札へ移動する動きも付けたいな。
デバッグ用に スペース・キーに割り当てるか」
void Update()
{ // ...
// デバッグ用
if (Input.GetKeyDown(KeyCode.Space))
{
// 両プレイヤーは手札から1枚抜いて、場札として置く
for (var player=0; player<2; player++)
{
MoveCardsToHandFromPile(player, 1);
}
}
}
「 👆 ゲーム・オブジェクトじゃなくて、画像素材側に設定するのかだぜ?」
「 👆 ウィンドウの縦幅を伸ばすと Apply
ボタンが出てきた。気づかね~」
📖 unityで画像背景を透過させる方法が分かりません。
📖 Unityでテクスチャを透明にする
「 👆 調べても分からんので昔ながらの方法でやる。GIMP を使って、
PNG画像が RGB 形式で、データが 不透明度を表す A チャンネルを持ってないようなので、
アルファ・チャンネルを追加し……」
「 👆 マジックワンドを使って 白い所を選んで、 [Del]
キーで消すぜ」
「 👆 この Shader
は Sprites/Diffuse
でいいのか?
自分が何やってんのか 分からなくて つらい」
📅2023-01-31 sat 06:12
「 👆 操作説明は これぐらい うるさく書いておけば 確かだろ。
邪魔くさいが……」
📅2023-01-31 sat 06:38
「 👆 やっぱ RGB値を 加算したいな……。
シェーダーの書き方を調べるか」
「 👆 日本人の薄っぺらい記事なんか読んでも さっぱり分からんな 公式読むか」
📖 カスタムシェーダーの基礎
📖 シェーダーの作成
📅2023-01-31 sat 06:58
「 しかし、今のプログラミングの書き方では ルール・ベースで書きにくい……」
「 👆 いったん 画面関係の変数は GameViewModel
クラスを新しく作って そっちへ移し……」
「 👆 GameManager.cs
の方では GameViewModel
インスタンスを使うように書き直し……」
「 👆 変数名の頭に go
と付けたやつは GameObject
なんで、
画面に関係するものは全部 GameViewModel
クラスへ 追いやるぜ」
「 👆 配列の長さを取りたいときは、 GetCenterStackCardsLength()
メソッドを使うようにする。
これを ラッピング・メソッド(Wrapping method) という」
「 👆 こうやって、 GameManager
の方は、 ゲーム・オブジェクトをいじらなくても、
player: 0
とか、 numberOfCards: 5
とか、命令だけ書けばいいような 見た目に変えていくぜ」
📅2023-01-31 sat 07:33
「 全部 ラッピングするの 時間かかるから 今朝は ここまで」
📅2023-01-31 sat 08:04
📅2023-02-01 sat 23:34 start
📅2023-02-01 sat 03:54 end
「 会社でやってはいけないことの1つが 大改造よ。
コード・レビュー受け付けられないから 没になるのよ」
「 👆 Unity に ゲームの基本機能が 足りな過ぎるので タイムライン機能を自作した」
「 👆 Command
(コマンド)というのは、プレイヤーができる操作だな。
中を見てみよう」
📄 Assets/Scripts/Models/Timeline/Commands/ICommand.cs
file:
namespace Assets.Scripts.Models.Timeline.Commands
{
using Assets.Scripts.Models;
using Assets.Scripts.Views;
/// <summary>
/// コマンド
/// </summary>
interface ICommand
{
/// <summary>
/// コマンド実行
/// </summary>
/// <param name="gameModelBuffer">ゲームの内部状態(編集可能)</param>
/// <param name="gameViewModel">画面表示の状態(編集可能)</param>
void DoIt(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel);
}
}
「 👆 『コマンドを実行すると、 ゲームの内部状態 と、 画面表示の状態 が変わる』、ということを書いている」
📄 Assets/Scripts/Models/Timeline/Commands/MoveCardsToHandFromPile.cs
file:
namespace Assets.Scripts.Models.Timeline.Commands
{
using Assets.Scripts.Models;
using Assets.Scripts.Views;
/// <summary>
/// nプレイヤーの手札から場札へ、m枚のカードを移動
/// </summary>
class MoveCardsToHandFromPile : ICommand
{
// - その他(生成)
/// <summary>
/// 生成
/// </summary>
/// <param name="player">nプレイヤー</param>
/// <param name="numberOfCards">カードがm枚</param>
internal MoveCardsToHandFromPile(int player, int numberOfCards)
{
Player = player;
NumberOfCards = numberOfCards;
}
// - プロパティ
int Player { get; set; }
int NumberOfCards { get; set; }
// - メソッド
/// <summary>
/// 手札の上の方からn枚抜いて、場札の後ろへ追加する
///
/// - 画面上の場札は位置調整される
/// </summary>
public void DoIt(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel)
{
// 手札の上の方からn枚抜いて、場札へ移動する
var length = gameModelBuffer.IdOfCardsOfPlayersPile[Player].Count; // 手札の枚数
if (NumberOfCards <= length)
{
// もし、場札が空っぽのところへ、手札を配ったのなら、先頭の場札をピックアップする
if (gameModelBuffer.IndexOfFocusedCardOfPlayers[Player] == -1)
{
gameModelBuffer.IndexOfFocusedCardOfPlayers[Player] = 0;
}
GameModel gameModel = new GameModel(gameModelBuffer);
var startIndex = length - NumberOfCards;
gameModelBuffer.MoveCardsToHandFromPile(Player, startIndex, NumberOfCards);
gameViewModel.ArrangeHandCards(gameModel, Player);
}
}
}
}
📄 Assets/Scripts/Models/Timeline/Commands/MoveCardsToPileFromCenterStacks.cs
file:
namespace Assets.Scripts.Models.Timeline.Commands
{
using Assets.Scripts.Models;
using Assets.Scripts.Views;
using System;
/// <summary>
/// 右(または左)側の台札1枚を、手札へ移動する
/// </summary>
class MoveCardsToPileFromCenterStacks : ICommand
{
// - 生成
internal MoveCardsToPileFromCenterStacks(int place)
{
this.Place = place;
}
// - プロパティ
int Place { get; set; }
// - メソッド
/// <summary>
/// 台札を、手札へ移動する
///
/// - ゲーム開始時に使う
/// </summary>
/// <param name="place">右:0, 左:1</param>
public void DoIt(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel)
{
// 台札の一番上(一番後ろ)のカードを1枚抜く
var numberOfCards = 1;
var length = gameModelBuffer.IdOfCardsOfCenterStacks[Place].Count; // 台札の枚数
if (1 <= length)
{
var startIndex = length - numberOfCards;
var idOfCard = gameModelBuffer.IdOfCardsOfCenterStacks[Place][startIndex];
gameModelBuffer.RemoveCardAtOfCenterStack(Place, startIndex);
// 黒いカードは1プレイヤー、赤いカードは2プレイヤー
int player;
float angleY;
var goCard = GameObjectStorage.PlayingCards[idOfCard];
if (goCard.name.StartsWith("Clubs") || goCard.name.StartsWith("Spades"))
{
player = 0;
angleY = 180.0f;
}
else if (goCard.name.StartsWith("Diamonds") || goCard.name.StartsWith("Hearts"))
{
player = 1;
angleY = 0.0f;
}
else
{
throw new Exception();
}
// プレイヤーの手札を積み上げる
gameModelBuffer.AddCardOfPlayersPile(player, idOfCard);
gameViewModel.SetPosRot(idOfCard, gameViewModel.pileCardsX[player], gameViewModel.pileCardsY[player], gameViewModel.pileCardsZ[player], angleY: angleY, angleZ: 180.0f);
gameViewModel.pileCardsY[player] += 0.2f;
}
}
}
}
📄 Assets/Scripts/Models/Timeline/Commands/MoveCardToCenterStackFromHand.cs
file:
namespace Assets.Scripts.Models.Timeline.Commands
{
using Assets.Scripts.Models;
using Assets.Scripts.Views;
/// <summary>
/// nプレイヤーがピックアップしている場札を、右(または左)の台札へ移動する
/// </summary>
class MoveCardToCenterStackFromHand : ICommand
{
// - 生成
internal MoveCardToCenterStackFromHand(int player, int place)
{
this.Player = player;
this.Place = place;
}
// - プロパティ
int Player { get; set; }
int Place { get; set; }
// - メソッド
/// <summary>
/// nプレイヤーがピックアップしている場札を、右(または左)の台札へ移動する
/// </summary>
/// <param name="player">何番目のプレイヤー</param>
/// <param name="place">右なら0、左なら1</param>
public void DoIt(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel)
{
var gameModel = new GameModel(gameModelBuffer);
// ピックアップしているカードがあるか?
GetIndexOfFocusedHandCard(
gameModelBuffer: gameModelBuffer,
player: Player,
(indexOfFocusedHandCard) =>
{
RemoveAtOfHandCard(
gameModelBuffer: gameModelBuffer,
gameViewModel: gameViewModel,
player: Player,
place: Place,
indexOfHandCardToRemove: indexOfFocusedHandCard,
setIndexOfNextFocusedHandCard: (indexOfNextFocusedHandCard) =>
{
gameModelBuffer.IndexOfFocusedCardOfPlayers[Player] = indexOfNextFocusedHandCard; // 更新:何枚目の場札をピックアップしているか
// 場札の位置調整
gameViewModel.ArrangeHandCards(
gameModel: gameModel,
player: Player);
});
});
}
private static void GetIndexOfFocusedHandCard(GameModelBuffer gameModelBuffer, int player, LazyArgs.SetValue<int> setIndex)
{
int handIndex = gameModelBuffer.IndexOfFocusedCardOfPlayers[player]; // 何枚目の場札をピックアップしているか
if (handIndex < 0 || gameModelBuffer.IdOfCardsOfPlayersHand[player].Count <= handIndex) // 範囲外は無視
{
return;
}
setIndex(handIndex);
}
/// <summary>
/// 台札を抜く
/// </summary>
/// <param name="player"></param>
/// <param name="indexOfHandCardToRemove"></param>
/// <param name="setIndexOfNextFocusedHandCard"></param>
private static void RemoveAtOfHandCard(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel, int player, int place, int indexOfHandCardToRemove, LazyArgs.SetValue<int> setIndexOfNextFocusedHandCard)
{
// 抜く前の場札の数
var lengthBeforeRemove = gameModelBuffer.IdOfCardsOfPlayersHand[player].Count;
if (indexOfHandCardToRemove < 0 || lengthBeforeRemove <= indexOfHandCardToRemove)
{
// 抜くのに失敗
return;
}
// 抜いた後の場札の数
var lengthAfterRemove = lengthBeforeRemove - 1;
// 抜いた後の次のピックアップするカードが先頭から何枚目か、先に算出
int indexOfNextFocusedHandCard;
if (lengthAfterRemove <= indexOfHandCardToRemove) // 範囲外アクセス防止対応
{
// 一旦、最後尾へ
indexOfNextFocusedHandCard = lengthAfterRemove - 1;
}
else
{
// そのまま
indexOfNextFocusedHandCard = indexOfHandCardToRemove;
}
var goCard = gameModelBuffer.IdOfCardsOfPlayersHand[player][indexOfHandCardToRemove]; // 場札を1枚抜いて
gameModelBuffer.RemoveCardAtOfPlayerHand(player, indexOfHandCardToRemove);
AddCardOfCenterStack2(gameModelBuffer, gameViewModel, goCard, place); // 台札
setIndexOfNextFocusedHandCard(indexOfNextFocusedHandCard);
}
private static void AddCardOfCenterStack2(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel, IdOfPlayingCards idOfCard, int place)
{
var gameModel = new GameModel(gameModelBuffer);
// 手ぶれ
var (shakeX, shakeZ, shakeAngleY) = gameViewModel.MakeShakeForCenterStack(place);
// 台札の次の天辺(一番後ろ)のカードの中心座標 X, Z
var (nextTopX, nextTopZ) = gameViewModel.GetXZOfNextCenterStackCard(gameModel, place);
// 台札の捻り
var goCard = GameObjectStorage.PlayingCards[idOfCard];
float nextAngleY = goCard.transform.rotation.eulerAngles.y;
var length = gameModel.GetLengthOfCenterStackCards(place);
if (length < 1)
{
}
else
{
nextAngleY += shakeAngleY;
}
gameModelBuffer.AddCardOfCenterStack(place, idOfCard); // 台札として置く
// 台札の位置をセット
gameViewModel.SetPosRot(idOfCard, nextTopX + shakeX, gameViewModel.centerStacksY[place], nextTopZ + shakeZ, angleY: nextAngleY);
// 次に台札に積むカードの高さ
gameViewModel.centerStacksY[place] += 0.2f;
}
}
}
📄 Assets/Scripts/Models/Timeline/Commands/MoveFocusToNextCard.cs
file:
namespace Assets.Scripts.Models.Timeline.Commands
{
using Assets.Scripts.Models;
using Assets.Scripts.Views;
using System;
/// <summary>
/// nプレイヤーは、右(または左)隣のカードへ、ピックアップを移動します
/// </summary>
class MoveFocusToNextCard : ICommand
{
// - 生成
internal MoveFocusToNextCard(int player, int direction, LazyArgs.SetValue<int> setIndexOfNextFocusedHandCard)
{
this.Player = player;
this.Direction = direction;
this.SetIndexOfNextFocusedHandCard = setIndexOfNextFocusedHandCard;
}
// - プロパティ
int Player { get; set; }
int Direction { get; set; }
LazyArgs.SetValue<int> SetIndexOfNextFocusedHandCard { get; set; }
// - メソッド
/// <summary>
/// nプレイヤーは、右(または左)隣のカードへ、ピックアップを移動します
/// </summary>
/// <param name="player"></param>
/// <param name="direction">後ろ:0, 前:1</param>
public void DoIt(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel)
{
GameModel gameModel = new GameModel(gameModelBuffer);
int indexOfFocusedHandCard = gameModelBuffer.IndexOfFocusedCardOfPlayers[Player];
int current;
var length = gameModelBuffer.IdOfCardsOfPlayersHand[Player].Count;
if (length < 1)
{
// 場札が無いなら、何もピックアップされていません
current = -1;
}
else
{
switch (Direction)
{
// 後ろへ
case 0:
if (indexOfFocusedHandCard == -1 || length <= indexOfFocusedHandCard + 1)
{
// (ピックアップしているカードが無いとき)先頭の外から、先頭へ入ってくる
current = 0;
}
else
{
current = indexOfFocusedHandCard + 1;
}
break;
// 前へ
case 1:
if (indexOfFocusedHandCard == -1 || indexOfFocusedHandCard - 1 < 0)
{
// (ピックアップしているカードが無いとき)最後尾の外から、最後尾へ入ってくる
current = length - 1;
}
else
{
current = indexOfFocusedHandCard - 1;
}
break;
default:
throw new Exception();
}
}
SetIndexOfNextFocusedHandCard(current);
if (0 <= indexOfFocusedHandCard && indexOfFocusedHandCard < gameModelBuffer.IdOfCardsOfPlayersHand[Player].Count) // 範囲内なら
{
// 前にフォーカスしていたカードを、盤に下ろす
gameViewModel.PutDownCardOfHand(gameModel, Player, indexOfFocusedHandCard);
}
if (0 <= current && current < gameModelBuffer.IdOfCardsOfPlayersHand[Player].Count) // 範囲内なら
{
// 今回フォーカスするカードを持ち上げる
gameViewModel.PickupCardOfHand(gameModel, Player, current);
}
}
}
}
「 👆 まあ、 だから ゲームの状態と、 画面の表示を 変更するのが コマンドだぜ」
「 👆 そして コマンドに 時間を付けて、 時限式で 実行すりゃいいんだぜ。
ソースを見てみよう」
📄 Assets/Scripts/Models/Timeline/TimedItem.cs
file:
namespace Assets.Scripts.Models.Timeline
{
using Assets.Scripts.Models.Timeline.Commands;
/// <summary>
/// 指定した時間と、そのとき実行されるコマンドのペア
/// </summary>
class TimedItem
{
// - その他(生成)
internal TimedItem(float seconds, ICommand command)
{
this.Seconds = seconds;
this.Command = command;
}
// - プロパティ
internal float Seconds { get; private set; }
internal ICommand Command { get; private set; }
}
}
「 👆 タイムラインの上に置いてあるコマンドだぜ。
『音符』みたいなもんだぜ。 感じろ」
📄 Assets/Scripts/Models/Timeline/Model.cs
file:
namespace Assets.Scripts.Models.Timeline
{
using Assets.Scripts.Models;
using Assets.Scripts.Models.Timeline.Commands;
using Assets.Scripts.Views;
using System.Collections.Generic;
/// <summary>
/// タイムライン・モデル
/// </summary>
internal class Model
{
// - プロパティ
List<TimedItem> timedItems = new();
internal List<TimedItem> TimedItems
{
get
{
return this.timedItems;
}
}
// - メソッド
/// <summary>
/// 追加
/// </summary>
/// <param name="seconds">実行される時間(秒)</param>
/// <param name="command">コマンド</param>
internal void Add(float seconds, ICommand command)
{
this.TimedItems.Add(new TimedItem(seconds,command));
}
/// <summary>
/// コマンドを消化
/// </summary>
/// <param name="elapsedSeconds">ゲーム内消費時間(秒)</param>
/// <param name="gameModelBuffer">ゲームの内部状態(編集可能)</param>
/// <param name="gameViewModel">画面表示の状態(編集可能)</param>
internal void DoIt(float elapsedSeconds, GameModelBuffer gameModelBuffer, GameViewModel gameViewModel)
{
if (0 < timedItems.Count)
{
var timedCommand = timedItems[0];
while (timedCommand.Seconds <= elapsedSeconds)
{
// 消化
timedItems.RemoveAt(0);
timedCommand.Command.DoIt(gameModelBuffer, gameViewModel);
if (0 < timedItems.Count)
{
timedCommand = timedItems[0];
}
else
{
break;
}
}
}
}
}
}
「 👆 タイムラインは、『音符』のようなものが記憶されていて、時間が来たら実行される。
『楽譜』みたいなもんだぜ。 感じろ」
「 👆 ゲームの状態を記憶しているのは、 GameModelBuffer
インスタンスだぜ。
GameModel
は、読み取り専用の GameModelBuffer
だぜ」
📄 Assets/Scripts/Models/GameModel.cs
file:
namespace Assets.Scripts.Models
{
using System.Collections.Generic;
/// <summary>
/// ゲーム・モデル
///
/// - 読み取り専用。(Immutable)
/// </summary>
class GameModel
{
GameModelBuffer gameModelBuffer;
public GameModel(GameModelBuffer gameModel)
{
this.gameModelBuffer = gameModel;
}
/// <summary>
/// 右(または左)の天辺の台札
/// </summary>
/// <param name="place">右:0, 左:1</param>
/// <returns></returns>
internal IdOfPlayingCards GetLastCardOfCenterStack(int place)
{
var length = this.GetLengthOfCenterStackCards(place);
var startIndex = length - 1;
return this.gameModelBuffer.IdOfCardsOfCenterStacks[place][startIndex]; // 最後のカード
}
/// <summary>
/// nプレイヤーが選択している場札は、先頭から何枚目
///
/// - 選択中の場札が無いなら、-1
/// </summary>
/// <param name="player">プレイヤー</param>
internal int GetIndexOfFocusedCardOfPlayer(int player)
{
return this.gameModelBuffer.IndexOfFocusedCardOfPlayers[player];
}
/// <summary>
/// 右(または左)の台札の枚数
/// </summary>
/// <param name="place">右:0, 左:1</param>
internal int GetLengthOfCenterStackCards(int place)
{
return this.gameModelBuffer.IdOfCardsOfCenterStacks[place].Count;
}
/// <summary>
/// nプレイヤーの、場札の枚数
/// </summary>
/// <param name="player">プレイヤー</param>
/// <returns></returns>
internal int GetLengthOfPlayerHandCards(int player)
{
return this.gameModelBuffer.IdOfCardsOfPlayersHand[player].Count;
}
/// <summary>
/// nプレイヤーの、場札をリストで取得
/// </summary>
/// <param name="player">プレイヤー</param>
/// <returns></returns>
internal List<IdOfPlayingCards> GetCardsOfPlayerHand(int player)
{
return this.gameModelBuffer.IdOfCardsOfPlayersHand[player];
}
/// <summary>
/// nプレイヤーの、m枚目の場札を取得
/// </summary>
/// <param name="player"></param>
/// <param name="handIndex"></param>
/// <returns></returns>
internal IdOfPlayingCards GetCardAtOfPlayerHand(int player, int handIndex)
{
return this.gameModelBuffer.IdOfCardsOfPlayersHand[player][handIndex];
}
}
}
「 👆 GameModel
は、GameModelBuffer
を包んでるわけだな」
📄 Assets/Scripts/Models/GameModelBuffer.cs
file:
namespace Assets.Scripts.Models
{
using System.Collections.Generic;
/// <summary>
/// ゲームの状態
///
/// - 編集可能
/// </summary>
public class GameModelBuffer
{
// - プロパティ
/// <summary>
/// nプレイヤーが選択している場札は、先頭から何枚目
///
/// - 選択中の場札が無いなら、-1
/// </summary>
internal int[] IndexOfFocusedCardOfPlayers { get; set; } = { -1, -1 };
/// <summary>
/// 手札
///
/// - プレイヤー側で積んでる札
/// - 0: 1プレイヤー(黒色)
/// - 1: 2プレイヤー(黒色)
/// </summary>
internal List<List<IdOfPlayingCards>> IdOfCardsOfPlayersPile { get; set; } = new() { new(), new() };
/// <summary>
/// 場札
///
/// - プレイヤー側でオープンしている札
/// - 0: 1プレイヤー(黒色)
/// - 1: 2プレイヤー(黒色)
/// </summary>
internal List<List<IdOfPlayingCards>> IdOfCardsOfPlayersHand { get; set; } = new() { new(), new() };
/// <summary>
/// 台札
///
/// - 画面中央に積んでいる札
/// - 0: 右
/// - 1: 左
/// </summary>
internal List<List<IdOfPlayingCards>> IdOfCardsOfCenterStacks { get; set; } = new() { new(), new() };
/// <summary>
/// 台札を削除
/// </summary>
/// <param name="place"></param>
/// <param name="startIndex"></param>
internal void RemoveCardAtOfCenterStack(int place, int startIndex)
{
this.IdOfCardsOfCenterStacks[place].RemoveAt(startIndex);
}
/// <summary>
/// 台札を追加
/// </summary>
/// <param name="place"></param>
/// <param name="idOfCard"></param>
internal void AddCardOfCenterStack(int place, IdOfPlayingCards idOfCard)
{
this.IdOfCardsOfCenterStacks[place].Add(idOfCard);
}
/// <summary>
/// 手札を追加
/// </summary>
/// <param name="player"></param>
/// <param name="idOfCard"></param>
internal void AddCardOfPlayersPile(int player, IdOfPlayingCards idOfCard)
{
this.IdOfCardsOfPlayersPile[player].Add(idOfCard);
}
/// <summary>
/// 手札を削除
/// </summary>
/// <param name="player"></param>
/// <param name="startIndex"></param>
/// <param name="numberOfCards"></param>
internal void RemoveRangeCardsOfPlayerPile(int player, int startIndex, int numberOfCards)
{
this.IdOfCardsOfPlayersPile[player].RemoveRange(startIndex, numberOfCards);
}
/// <summary>
/// 場札を追加
/// </summary>
/// <param name="player"></param>
/// <param name="idOfCards"></param>
internal void AddRangeCardsOfPlayerHand(int player, List<IdOfPlayingCards> idOfCards)
{
this.IdOfCardsOfPlayersHand[player].AddRange(idOfCards);
}
/// <summary>
/// 場札を削除
/// </summary>
/// <param name="player"></param>
/// <param name="handIndex"></param>
internal void RemoveCardAtOfPlayerHand(int player, int handIndex)
{
this.IdOfCardsOfPlayersHand[player].RemoveAt(handIndex);
}
/// <summary>
/// 手札から場札へ移動
/// </summary>
/// <param name="player"></param>
/// <param name="startIndex"></param>
/// <param name="numberOfCards"></param>
internal void MoveCardsToHandFromPile(int player, int startIndex, int numberOfCards)
{
var idOfCards = this.IdOfCardsOfPlayersPile[player].GetRange(startIndex, numberOfCards);
this.RemoveRangeCardsOfPlayerPile(player, startIndex, numberOfCards);
this.AddRangeCardsOfPlayerHand(player, idOfCards);
}
}
}
「 👆 GameModelBuffer
は、ゲームを時間で切った断面図みたいなもんだぜ」
📄 Assets/Scripts/Models/IdOfPlayingCards.cs
file:
namespace Assets.Scripts.Models
{
/// <summary>
/// トランプのカード
///
/// - ジョーカーを除く
/// </summary>
internal enum IdOfPlayingCards
{
Clubs1,
Clubs2,
Clubs3,
Clubs4,
Clubs5,
Clubs6,
Clubs7,
Clubs8,
Clubs9,
Clubs10,
Clubs11,
Clubs12,
Clubs13,
Diamonds1,
Diamonds2,
Diamonds3,
Diamonds4,
Diamonds5,
Diamonds6,
Diamonds7,
Diamonds8,
Diamonds9,
Diamonds10,
Diamonds11,
Diamonds12,
Diamonds13,
Hearts1,
Hearts2,
Hearts3,
Hearts4,
Hearts5,
Hearts6,
Hearts7,
Hearts8,
Hearts9,
Hearts10,
Hearts11,
Hearts12,
Hearts13,
Spades1,
Spades2,
Spades3,
Spades4,
Spades5,
Spades6,
Spades7,
Spades8,
Spades9,
Spades10,
Spades11,
Spades12,
Spades13,
}
}
「 👆 トランプのカードの Id を、 enum型で作っておくぜ」
📅2023-02-01 sat 22:04
📄 Assets/Scripts/Views/GameObjectStorage.cs
file:
namespace Assets.Scripts.Views
{
using Assets.Scripts.Models;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// ゲーム・オブジェクトと、その Id の紐づけ
/// </summary>
static class GameObjectStorage
{
internal static Dictionary<IdOfPlayingCards, GameObject> PlayingCards { get; private set; } = new();
internal static void Add(IdOfPlayingCards cardId, GameObject goCard)
{
PlayingCards.Add(cardId, goCard);
}
}
}
「 👆 ゲーム・オブジェクトを、 Id で すぐ取り出せる仕組みを作っておくぜ。
GameObject.Find( ... )
は 処理が重たいらしいしな」
📄 Assets/Scripts/Views/GameViewModel.cs
file:
namespace Assets.Scripts.Views
{
using Assets.Scripts.Models;
using System;
using UnityEngine;
/// <summary>
/// 画面表示関連
///
/// 西端: -62.0f
/// 東端: 62.0f
/// </summary>
public class GameViewModel
{
// - プロパティー
/// <summary>
/// 底端
///
/// - `0.0f` は盤
/// </summary>
internal readonly float minY = 0.5f;
internal readonly float[] handCardsZ = new[] { -28.0f, 42.0f };
// 手札(プレイヤー側で伏せて積んでる札)
internal readonly float[] pileCardsX = new[] { 40.0f, -40.0f }; // 端っこは 62.0f, -62.0f
internal readonly float[] pileCardsY = new[] { 0.5f, 0.5f };
internal readonly float[] pileCardsZ = new[] { -6.5f, 16.0f };
// 台札
internal float[] centerStacksX = { 15.0f, -15.0f };
/// <summary>
/// 台札のY座標
///
/// - 右が 0、左が 1
/// - 0.0f は盤なので、それより上にある
/// </summary>
internal float[] centerStacksY = { 0.5f, 0.5f };
internal float[] centerStacksZ = { 2.5f, 9.0f };
// - メソッド
/// <summary>
/// 台札の次の天辺の位置
/// </summary>
/// <param name="place"></param>
/// <returns></returns>
internal (float, float) GetXZOfNextCenterStackCard(GameModel gameModel, int place)
{
var length = gameModel.GetLengthOfCenterStackCards(place);
if (length < 1)
{
// 床上
var nextTopX2 = this.centerStacksX[place];
var nextTopZ2 = this.centerStacksZ[place];
return (nextTopX2, nextTopZ2);
}
// 台札の次の天辺の位置
var idOfLastCard = gameModel.GetLastCardOfCenterStack(place); // 天辺(最後)のカード
var goLastCard = GameObjectStorage.PlayingCards[idOfLastCard];
var nextTopX = (this.centerStacksX[place] - goLastCard.transform.position.x) / 2 + this.centerStacksX[place];
var nextTopZ = (this.centerStacksZ[place] - goLastCard.transform.position.z) / 2 + this.centerStacksZ[place];
return (nextTopX, nextTopZ);
}
/// <summary>
/// 場札を持ち上げる
/// </summary>
/// <param name="player"></param>
/// <param name="handIndesx"></param>
internal void PickupCardOfHand(GameModel gameModel, int player, int handIndesx)
{
var idOfFocusedHandCard = gameModel.GetCardAtOfPlayerHand(player, handIndesx);
var liftY = 5.0f; // 持ち上げる(パースペクティブがかかっていて、持ち上げすぎると北へ移動したように見える)
var rotateY = -5; // -5°傾ける
var rotateZ = -5; // -5°傾ける
var goCard = GameObjectStorage.PlayingCards[idOfFocusedHandCard];
goCard.transform.position = new Vector3(goCard.transform.position.x, goCard.transform.position.y + liftY, goCard.transform.position.z);
goCard.transform.rotation = Quaternion.Euler(goCard.transform.rotation.eulerAngles.x, goCard.transform.rotation.eulerAngles.y + rotateY, goCard.transform.eulerAngles.z + rotateZ);
}
/// <summary>
/// ピックアップしているカードを場に戻す
/// </summary>
/// <param name="card"></param>
internal void PutDownCardOfHand(GameModel gameModel, int player, int handIndex)
{
var idOfCard = gameModel.GetCardAtOfPlayerHand(player, handIndex);
var liftY = 5.0f; // 持ち上げる(パースペクティブがかかっていて、持ち上げすぎると北へ移動したように見える)
var rotateY = -5; // -5°傾ける
var rotateZ = -5; // -5°傾ける
// 逆をする
liftY = -liftY;
rotateY = -rotateY;
rotateZ = -rotateZ;
var goCard = GameObjectStorage.PlayingCards[idOfCard];
goCard.transform.position = new Vector3(goCard.transform.position.x, goCard.transform.position.y + liftY, goCard.transform.position.z);
goCard.transform.rotation = Quaternion.Euler(goCard.transform.rotation.eulerAngles.x, goCard.transform.rotation.eulerAngles.y + rotateY, goCard.transform.eulerAngles.z + rotateZ);
}
/// <summary>
/// 場札を並べる
///
/// - 左端は角度で言うと 112.0f
/// </summary>
internal void ArrangeHandCards(GameModel gameModel, int player)
{
int handIndex = gameModel.GetIndexOfFocusedCardOfPlayer(player);
// 25枚の場札が並べるように調整してある
int numberOfCards = gameModel.GetLengthOfPlayerHandCards(player); // 場札の枚数
if (numberOfCards < 1)
{
return; // 何もしない
}
float cardAngleZ = -5; // カードの少しの傾き
int range = 200; // 半径。大きな円にするので、中心を遠くに離したい
int offsetCircleCenterZ; // 中心位置の調整
float angleY;
float playerTheta;
float angleStep = -1.83f;
float startTheta = (numberOfCards * Mathf.Abs(angleStep) / 2 - Mathf.Abs(angleStep) / 2 + 90.0f) * Mathf.Deg2Rad;
float thetaStep = angleStep * Mathf.Deg2Rad; ; // 時計回り
float ox = 0.0f;
float oz = this.handCardsZ[player];
switch (player)
{
case 0:
// 1プレイヤー
angleY = 180.0f;
playerTheta = 0;
offsetCircleCenterZ = -190;
break;
case 1:
// 2プレイヤー
angleY = 0.0f;
playerTheta = 180 * Mathf.Deg2Rad;
offsetCircleCenterZ = 188; // カメラのパースペクティブが付いているから、目視で調整
break;
default:
throw new Exception();
}
float theta = startTheta;
foreach (var goCard in gameModel.GetCardsOfPlayerHand(player))
{
float x = range * Mathf.Cos(theta + playerTheta) + ox;
float z = range * Mathf.Sin(theta + playerTheta) + oz + offsetCircleCenterZ;
SetPosRot(goCard, x, this.minY, z, angleY: angleY, angleZ: cardAngleZ);
theta += thetaStep;
}
// 場札を並べなおすと、持ち上げていたカードを下ろしてしまうので、再度、持ち上げる
this.ResumeCardPickup(gameModel, player);
}
/// <summary>
/// 場札を並べなおすと、持ち上げていたカードを下ろしてしまうので、再度、持ち上げる
/// </summary>
private void ResumeCardPickup(GameModel gameModel, int player)
{
int handIndex = gameModel.GetIndexOfFocusedCardOfPlayer(player);
if (0 <= handIndex && handIndex < gameModel.GetLengthOfPlayerHandCards(player)) // 範囲内なら
{
// 抜いたカードの右隣のカードを(有れば)ピックアップする
this.PickupCardOfHand(gameModel, player, handIndex);
}
}
/// <summary>
/// カードの位置と 捻りの設定
/// </summary>
/// <param name="card"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="z"></param>
/// <param name="angleY"></param>
/// <param name="angleZ"></param>
/// <param name="motionProgress">Update関数の中でないと役に立たない</param>
internal void SetPosRot(IdOfPlayingCards idOfCard, float x, float y, float z, float angleY = 180.0f, float angleZ = 0.0f, float motionProgress = 1.0f)
{
var goCard = GameObjectStorage.PlayingCards[idOfCard];
var beginPos = goCard.transform.position;
var endPos = new Vector3(x, y, z);
goCard.transform.position = Vector3.Lerp(beginPos, endPos, motionProgress);
goCard.transform.rotation = Quaternion.Euler(0, angleY, angleZ);
}
/// <summary>
/// ぴったり積むと不自然だから、X と Z を少しずらすための仕組み
///
/// - 1プレイヤー、2プレイヤーのどちらも右利きと仮定
/// </summary>
/// <param name="player"></param>
/// <returns></returns>
internal (float, float, float) MakeShakeForCenterStack(int player)
{
// 1プレイヤーから見て。左上にずれていくだろう
var left = -1.5f;
var right = 0.5f;
var bottom = -0.5f;
var top = 1.5f;
var angleY = UnityEngine.Random.Range(-10, 40); // 反時計回りに大きく捻りそう
switch (player)
{
case 0:
return (UnityEngine.Random.Range(left, right), UnityEngine.Random.Range(bottom, top), angleY);
case 1:
return (UnityEngine.Random.Range(-right, -left), UnityEngine.Random.Range(-top, -bottom), angleY);
default:
throw new Exception();
}
}
}
}
「 👆 ゲーム・ビュー・モデルは 画面の表示が どんな感じになってるか記憶したり、編集したりしているな」
「 👆 一番上の階層のスクリプトは、整理できてないものが残っている」
📄 Assets/Scripts/PlayingCard.cs
file:
using UnityEngine;
public class PlayingCard : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
// 今回は使わない
//
///// <summary>
///// マウスボタン押下時
///// </summary>
//private void OnMouseDown()
//{
// // 裏返します
// var oldZ = transform.rotation.eulerAngles.z; // 度数法
// transform.rotation = Quaternion.Euler(0, 0, oldZ + 180); // 180°回転
//}
}
「 👆 PlayingCard.cs
は、神経衰弱ゲームのとき使っていたが、スピードでは使っていないぜ」
📄 Assets/Scripts/LazyArgs.cs
file:
namespace Assets.Scripts
{
/// <summary>
/// コーディングのテクニックのための仕込み
/// </summary>
internal class LazyArgs
{
public delegate void Action();
public delegate void SetValue<T>(T value);
}
}
「 👆 LazyArgs.cs
は、コードを上手く書くテクニックに使うだけなんで、気にしなくていい」
📄 Assets/Scripts/GameManager.cs
file:
using Assets.Scripts.Models;
using Assets.Scripts.Views;
using System;
using System.Linq;
using UnityEngine;
using Commands = Assets.Scripts.Models.Timeline.Commands;
using ModelsOfTimeline = Assets.Scripts.Models.Timeline;
/// <summary>
/// ゲーム・マネージャー
///
/// - スピードは、日本と海外で ルールとプレイング・スタイルに違いがあるので、用語に統一感はない
/// </summary>
public class GameManager : MonoBehaviour
{
ModelsOfTimeline.Model commandStorage;
GameModelBuffer gameModelBuffer;
GameModel gameModel;
GameViewModel gameViewModel;
// ゲーム内単位時間
float unitSeconds = 1.0f / 60.0f;
// ゲーム内経過時間
float elapsedSeconds = 0.0f;
// Start is called before the first frame update
void Start()
{
// 全てのカードのゲーム・オブジェクトを、IDに紐づける
GameObjectStorage.Add(IdOfPlayingCards.Clubs1, GameObject.Find($"Clubs 1"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs2, GameObject.Find($"Clubs 2"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs3, GameObject.Find($"Clubs 3"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs4, GameObject.Find($"Clubs 4"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs5, GameObject.Find($"Clubs 5"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs6, GameObject.Find($"Clubs 6"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs7, GameObject.Find($"Clubs 7"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs8, GameObject.Find($"Clubs 8"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs9, GameObject.Find($"Clubs 9"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs10, GameObject.Find($"Clubs 10"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs11, GameObject.Find($"Clubs 11"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs12, GameObject.Find($"Clubs 12"));
GameObjectStorage.Add(IdOfPlayingCards.Clubs13, GameObject.Find($"Clubs 13"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds1, GameObject.Find($"Diamonds 1"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds2, GameObject.Find($"Diamonds 2"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds3, GameObject.Find($"Diamonds 3"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds4, GameObject.Find($"Diamonds 4"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds5, GameObject.Find($"Diamonds 5"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds6, GameObject.Find($"Diamonds 6"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds7, GameObject.Find($"Diamonds 7"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds8, GameObject.Find($"Diamonds 8"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds9, GameObject.Find($"Diamonds 9"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds10, GameObject.Find($"Diamonds 10"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds11, GameObject.Find($"Diamonds 11"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds12, GameObject.Find($"Diamonds 12"));
GameObjectStorage.Add(IdOfPlayingCards.Diamonds13, GameObject.Find($"Diamonds 13"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts1, GameObject.Find($"Hearts 1"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts2, GameObject.Find($"Hearts 2"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts3, GameObject.Find($"Hearts 3"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts4, GameObject.Find($"Hearts 4"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts5, GameObject.Find($"Hearts 5"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts6, GameObject.Find($"Hearts 6"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts7, GameObject.Find($"Hearts 7"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts8, GameObject.Find($"Hearts 8"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts9, GameObject.Find($"Hearts 9"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts10, GameObject.Find($"Hearts 10"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts11, GameObject.Find($"Hearts 11"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts12, GameObject.Find($"Hearts 12"));
GameObjectStorage.Add(IdOfPlayingCards.Hearts13, GameObject.Find($"Hearts 13"));
GameObjectStorage.Add(IdOfPlayingCards.Spades1, GameObject.Find($"Spades 1"));
GameObjectStorage.Add(IdOfPlayingCards.Spades2, GameObject.Find($"Spades 2"));
GameObjectStorage.Add(IdOfPlayingCards.Spades3, GameObject.Find($"Spades 3"));
GameObjectStorage.Add(IdOfPlayingCards.Spades4, GameObject.Find($"Spades 4"));
GameObjectStorage.Add(IdOfPlayingCards.Spades5, GameObject.Find($"Spades 5"));
GameObjectStorage.Add(IdOfPlayingCards.Spades6, GameObject.Find($"Spades 6"));
GameObjectStorage.Add(IdOfPlayingCards.Spades7, GameObject.Find($"Spades 7"));
GameObjectStorage.Add(IdOfPlayingCards.Spades8, GameObject.Find($"Spades 8"));
GameObjectStorage.Add(IdOfPlayingCards.Spades9, GameObject.Find($"Spades 9"));
GameObjectStorage.Add(IdOfPlayingCards.Spades10, GameObject.Find($"Spades 10"));
GameObjectStorage.Add(IdOfPlayingCards.Spades11, GameObject.Find($"Spades 11"));
GameObjectStorage.Add(IdOfPlayingCards.Spades12, GameObject.Find($"Spades 12"));
GameObjectStorage.Add(IdOfPlayingCards.Spades13, GameObject.Find($"Spades 13"));
commandStorage = new ModelsOfTimeline.Model();
gameModelBuffer = new GameModelBuffer();
gameModel = new GameModel(gameModelBuffer);
gameViewModel = new GameViewModel();
// ゲーム開始時、とりあえず、すべてのカードは、いったん右の台札という扱いにする
const int right = 0;// 台札の右
// const int left = 1;// 台札の左
foreach (var idOfCard in GameObjectStorage.PlayingCards.Keys)
{
// 右の台札
gameModelBuffer.IdOfCardsOfCenterStacks[right].Add(idOfCard);
}
// 右の台札をシャッフル
gameModelBuffer.IdOfCardsOfCenterStacks[right] = gameModelBuffer.IdOfCardsOfCenterStacks[right].OrderBy(i => Guid.NewGuid()).ToList();
// 右の台札をすべて、色分けして、黒色なら1プレイヤーの、赤色なら2プレイヤーの、手札に乗せる
while (0 < gameModel.GetLengthOfCenterStackCards(right))
{
// 即実行
new Commands.MoveCardsToPileFromCenterStacks(place: right).DoIt(gameModelBuffer, gameViewModel);
}
// 1,2プレイヤーについて、手札から5枚抜いて、場札として置く(画面上の場札の位置は調整される)
var time = 0.0f;
this.commandStorage.Add(time, new Commands.MoveCardsToHandFromPile(player: 0, numberOfCards: 5));
this.commandStorage.Add(time, new Commands.MoveCardsToHandFromPile(player: 1, numberOfCards: 5));
// 以下、デモ・プレイを登録
SetupDemo();
// OnTick を 1.0 秒後に呼び出し、以降は unitSeconds 秒毎に実行
InvokeRepeating(nameof(OnTick), 1.0f, unitSeconds);
}
// Update is called once per frame
void Update()
{
// 入力をコマンドとして登録
UpdateInput();
}
/// <summary>
/// 一定間隔で呼び出される
/// </summary>
void OnTick()
{
// 時限式で、コマンドを消化
this.commandStorage.DoIt(elapsedSeconds, gameModelBuffer, gameViewModel);
elapsedSeconds += unitSeconds;
}
/// <summary>
/// 入力を、コマンドに変換して、タイムラインへ登録します
/// </summary>
private void UpdateInput()
{
const int right = 0;// 台札の右
const int left = 1;// 台札の左
bool handled1player = false;
bool handled2player = false;
// 先に登録したコマンドの方が早く実行される
// (ボタン押下が同時なら)右の台札は1プレイヤー優先
// ==================================================
if (Input.GetKeyDown(KeyCode.DownArrow))
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
this.commandStorage.Add(elapsedSeconds, new Commands.MoveCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
place: right // 右の
));
handled1player = true;
}
if (Input.GetKeyDown(KeyCode.W))
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
this.commandStorage.Add(elapsedSeconds, new Commands.MoveCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
place: right // 右の
));
handled2player = true;
}
// (ボタン押下が同時なら)左の台札は2プレイヤー優先
// ==================================================
// 2プレイヤー
if (Input.GetKeyDown(KeyCode.S))
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)左の台札へ積み上げる
this.commandStorage.Add(elapsedSeconds, new Commands.MoveCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
place: left // 左の
));
handled2player = true;
}
// 1プレイヤー
if (Input.GetKeyDown(KeyCode.UpArrow))
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)左の台札へ積み上げる
this.commandStorage.Add(elapsedSeconds, new Commands.MoveCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
place: left // 左の
));
handled1player = true;
}
// それ以外のキー入力は、同時でも勝敗に関係しない
// ==============================================
// 1プレイヤー
if(handled1player)
{
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
// 1プレイヤーのピックアップしているカードから見て、(1プレイヤーから見て)左隣のカードをピックアップするように変えます
var player = 0;
this.commandStorage.Add(elapsedSeconds, new Commands.MoveFocusToNextCard(
player: player,
direction: 1,
setIndexOfNextFocusedHandCard: (indexOfNextFocusedHandCard) =>
{
gameModelBuffer.IndexOfFocusedCardOfPlayers[player] = indexOfNextFocusedHandCard; // 更新
}));
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
// 1プレイヤーのピックアップしているカードから見て、(1プレイヤーから見て)右隣のカードをピックアップするように変えます
var player = 0;
this.commandStorage.Add(elapsedSeconds, new Commands.MoveFocusToNextCard(
player: player,
direction: 0,
setIndexOfNextFocusedHandCard: (indexOfNextFocusedHandCard) =>
{
gameModelBuffer.IndexOfFocusedCardOfPlayers[player] = indexOfNextFocusedHandCard; // 更新
}));
}
// 2プレイヤー
if(handled2player)
{
}
else if (Input.GetKeyDown(KeyCode.A))
{
// 2プレイヤーのピックアップしているカードから見て、(2プレイヤーから見て)左隣のカードをピックアップするように変えます
var player = 1;
this.commandStorage.Add(elapsedSeconds, new Commands.MoveFocusToNextCard(
player: player,
direction: 1,
setIndexOfNextFocusedHandCard: (indexOfNextFocusedHandCard) =>
{
gameModelBuffer.IndexOfFocusedCardOfPlayers[player] = indexOfNextFocusedHandCard; // 更新
}));
}
else if (Input.GetKeyDown(KeyCode.D))
{
// 2プレイヤーのピックアップしているカードから見て、(2プレイヤーから見て)右隣のカードをピックアップするように変えます
var player = 1;
this.commandStorage.Add(elapsedSeconds, new Commands.MoveFocusToNextCard(
player: player,
direction: 0,
setIndexOfNextFocusedHandCard: (indexOfNextFocusedHandCard) =>
{
gameModelBuffer.IndexOfFocusedCardOfPlayers[player] = indexOfNextFocusedHandCard; // 更新
}));
}
// デバッグ用
if (Input.GetKeyDown(KeyCode.Space))
{
// 両プレイヤーは手札から1枚抜いて、場札として置く
for (var player = 0; player < 2; player++)
{
// 場札を並べる
this.commandStorage.Add(elapsedSeconds, new Commands.MoveCardsToHandFromPile(
player: player,
numberOfCards: 1));
}
}
}
/// <summary>
/// タイムライン作成
///
/// - デモ
/// </summary>
void SetupDemo()
{
// 卓準備
const int right = 0;// 台札の右
const int left = 1;// 台札の左
float scheduleSeconds = 1.0f;
float oneSecond = 1.0f;
// 登録:ピックアップ場札を、台札へ積み上げる
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、右の台札へ積み上げる
this.commandStorage.Add(scheduleSeconds, new Commands.MoveCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
place: right // 右の
));
// 2プレイヤーが、ピックアップ中の場札を抜いて、左の台札へ積み上げる
this.commandStorage.Add(scheduleSeconds, new Commands.MoveCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
place: left // 左の
));
scheduleSeconds += oneSecond;
}
// ゲーム・デモ開始
// 登録:カード選択
{
for (int i = 0; i < 2; i++)
{
// 1プレイヤーの右隣のカードへフォーカスを移します
{
var player = 0;
this.commandStorage.Add(scheduleSeconds, new Commands.MoveFocusToNextCard(
player: player,
direction: 0,
setIndexOfNextFocusedHandCard: (indexOfNextFocusedHandCard) =>
{
gameModelBuffer.IndexOfFocusedCardOfPlayers[player] = indexOfNextFocusedHandCard; // 更新
}));
}
// 2プレイヤーの右隣のカードへフォーカスを移します
{
var player = 1;
this.commandStorage.Add(scheduleSeconds, new Commands.MoveFocusToNextCard(
player: player,
direction: 0,
setIndexOfNextFocusedHandCard: (indexOfNextFocusedHandCard) =>
{
gameModelBuffer.IndexOfFocusedCardOfPlayers[player] = indexOfNextFocusedHandCard; // 更新
}));
}
scheduleSeconds += oneSecond;
}
}
// 登録:台札を積み上げる
{
this.commandStorage.Add(scheduleSeconds, new Commands.MoveCardToCenterStackFromHand(
player: 0, // 1プレイヤーが
place: 1 // 左の台札
));
this.commandStorage.Add(scheduleSeconds, new Commands.MoveCardToCenterStackFromHand(
player: 1, // 2プレイヤーが
place: 0 // 右の台札
));
scheduleSeconds += oneSecond;
}
// 登録:手札から1枚引く
{
// 1プレイヤーは手札から1枚抜いて、場札として置く
this.commandStorage.Add(scheduleSeconds, new Commands.MoveCardsToHandFromPile(
player: 0,
numberOfCards: 1));
// 2プレイヤーは手札から1枚抜いて、場札として置く
this.commandStorage.Add(scheduleSeconds, new Commands.MoveCardsToHandFromPile(
player: 1,
numberOfCards: 1));
scheduleSeconds += oneSecond;
}
}
}
「 👆 キー入力しても、コマンドは ただちに実行せず、
いったん タイムラインに登録するというのが、
ビューと モデルを分離した工夫だぜ」
📅2023-02-01 sat 22:24
「 Lerp
(リープ) を使うと、モーションを補間できるんじゃないの?」
📄 Assets/Scripts/GameManager.cs
file:
/// <summary>
/// 場札を持ち上げる
/// </summary>
/// <param name="player"></param>
/// <param name="handIndesx"></param>
internal void PickupCardOfHand(GameModel gameModel, int player, int handIndesx)
{
var idOfFocusedHandCard = gameModel.GetCardAtOfPlayerHand(player, handIndesx);
var liftY = 5.0f; // 持ち上げる(パースペクティブがかかっていて、持ち上げすぎると北へ移動したように見える)
var rotateY = -5; // -5°傾ける
var rotateZ = -5; // -5°傾ける
var goCard = GameObjectStorage.PlayingCards[idOfFocusedHandCard];
goCard.transform.position = new Vector3(
goCard.transform.position.x,
goCard.transform.position.y + liftY,
goCard.transform.position.z);
goCard.transform.rotation = Quaternion.Euler(
goCard.transform.rotation.eulerAngles.x,
goCard.transform.rotation.eulerAngles.y + rotateY,
goCard.transform.eulerAngles.z + rotateZ);
}
「 👆 例えば、場札を持ち上げるのは、 position
や rotation
を上書きしていたが、
これをやめて、
持ち上げる前の position
と rotation
、
持ち上げた後の position
と rotation
を持てばいいわけだぜ」
「 それを覚えておいて、 Update
メソッドで Lerp()
すればいいわけだぜ」
/// <summary>
/// 場札を持ち上げる
/// </summary>
/// <param name="player"></param>
/// <param name="handIndesx"></param>
internal void PickupCardOfHand(GameModel gameModel, int player, int handIndesx)
{
var idOfFocusedHandCard = gameModel.GetCardAtOfPlayerHand(player, handIndesx);
var liftY = 5.0f; // 持ち上げる(パースペクティブがかかっていて、持ち上げすぎると北へ移動したように見える)
var rotateY = -5; // -5°傾ける
var rotateZ = -5; // -5°傾ける
var goCard = GameObjectStorage.PlayingCards[idOfFocusedHandCard];
var beginPosition = goCard.transform.position;
var endPosition = new Vector3(
goCard.transform.position.x,
goCard.transform.position.y + liftY,
goCard.transform.position.z);
var beginRotation = goCard.transform.rotation;
var endRotation = Quaternion.Euler(
goCard.transform.rotation.eulerAngles.x,
goCard.transform.rotation.eulerAngles.y + rotateY,
goCard.transform.eulerAngles.z + rotateZ);
// TODO ★ セットせず、 Lerp したい
goCard.transform.position = endPosition;
goCard.transform.rotation = endRotation;
}
「 👆 この beginPosition
、 endPosition
、 beginRotation
、 endRotation
を
呼出し元へ さかのぼって持っていけばいいのか、大変だな」
📺 開発中画面
📅 2023-02-01 sat 23:58 end
📺 作業用BGM
「 👆 とりあえず、 Movement
というクラスを作ろうぜ」
Assets/Scripts/Models/Timeline/Movement.cs
file:
namespace Assets.Scripts.Models.Timeline
{
using UnityEngine;
/// <summary>
/// ゲーム・オブジェクトの動き
///
/// - Lerpに使うもの
/// </summary>
internal class Movement
{
// - その他(生成)
/// <summary>
/// 生成
/// </summary>
/// <param name="beginPosition">開始位置</param>
/// <param name="endPosition">終了位置</param>
/// <param name="beginRotation">開始回転</param>
/// <param name="endRotation">終了回転</param>
/// <param name="gameObject">ゲーム・オブジェクト</param>
public Movement(
Vector3 beginPosition,
Vector3 endPosition,
Quaternion beginRotation,
Quaternion endRotation,
GameObject gameObject)
{
this.BeginPosition = beginPosition;
this.EndPosition = endPosition;
this.BeginRotation = beginRotation;
this.EndRotation = endRotation;
this.GameObject = gameObject;
}
// - プロパティ
internal Vector3 BeginPosition { get; private set; }
internal Vector3 EndPosition { get; private set; }
internal Quaternion BeginRotation { get; private set; }
internal Quaternion EndRotation { get; private set; }
internal GameObject GameObject { get; private set; }
}
}
書き直す前のソース:
/// <summary>
/// カードの位置と 捻りの設定
/// </summary>
/// <param name="card"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="z"></param>
/// <param name="angleY"></param>
/// <param name="angleZ"></param>
/// <param name="motionProgress">Update関数の中でないと役に立たない</param>
internal void SetPosRot(IdOfPlayingCards idOfCard, float x, float y, float z, float angleY = 180.0f, float angleZ = 0.0f, float motionProgress = 1.0f)
{
var goCard = GameObjectStorage.PlayingCards[idOfCard];
var beginPos = goCard.transform.position;
var endPos = new Vector3(x, y, z);
goCard.transform.position = Vector3.Lerp(beginPos, endPos, motionProgress);
goCard.transform.rotation = Quaternion.Euler(0, angleY, angleZ);
}
書き直した後のソース:
/// <summary>
/// カードの位置と 捻りの設定
/// </summary>
/// <param name="card"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="z"></param>
/// <param name="angleY"></param>
/// <param name="angleZ"></param>
/// <param name="motionProgress">Update関数の中でないと役に立たない</param>
internal void SetPosRot(IdOfPlayingCards idOfCard, float x, float y, float z, float angleY = 180.0f, float angleZ = 0.0f, float motionProgress = 1.0f)
{
var goCard = GameObjectStorage.PlayingCards[idOfCard];
var movement = new Movement(
beginPosition: goCard.transform.position,
endPosition: new Vector3(x, y, z),
beginRotation: goCard.transform.rotation,
endRotation: Quaternion.Euler(0, angleY, angleZ),
gameObject: goCard);
goCard.transform.position = Vector3.Lerp(movement.BeginPosition, movement.EndPosition, motionProgress);
goCard.transform.rotation = Quaternion.Lerp(movement.BeginRotation, movement.EndRotation, motionProgress);
}
「 👆 動作が変わるんで リファクタリング ではなくて 仕様変更だが、
どんどん Lerp
するコードを 関数の外側に出すための 仕込みをしていこう」
「 👆 SetPosRot
という関数そのものが よくないので、この関数は削除して
呼出し側に ベタ書き するように変えていこう」
「 最大25枚の場札を 円弧上に 揃えて並べていく処理よ それ」
「 じゃあ、タイムラインには コマンドだけではなくて、
Movement
も置けた方がいいのか」
「 ICommand
と、 Movement
を、1本化しろだぜ」
Assets/Scripts/Models/Timeline/Commands/ICommand.cs
file:
書き直す前のソース:
namespace Assets.Scripts.Models.Timeline.Commands
{
using Assets.Scripts.Models;
using Assets.Scripts.Views;
/// <summary>
/// コマンド
/// </summary>
interface ICommand
{
/// <summary>
/// コマンド実行
/// </summary>
/// <param name="gameModelBuffer">ゲームの内部状態(編集可能)</param>
/// <param name="gameViewModel">画面表示の状態(編集可能)</param>
void DoIt(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel);
void Lerp(float progress);
/// <summary>
/// 持続時間が切れたとき
/// </summary>
void OnLeave();
}
}
書き直した後のソース:
namespace Assets.Scripts.Models.Timeline.Commands
{
using Assets.Scripts.Models;
using Assets.Scripts.Views;
/// <summary>
/// コマンド
/// </summary>
interface ICommand
{
/// <summary>
/// 開始時
/// </summary>
/// <param name="gameModelBuffer">ゲームの内部状態(編集可能)</param>
/// <param name="gameViewModel">画面表示の状態(編集可能)</param>
void OnEnter(GameModelBuffer gameModelBuffer, GameViewModel gameViewModel);
/// <summary>
/// 持続中
/// </summary>
/// <param name="progress">進捗 0.0 ~ 1.0</param>
void Lerp(float progress);
/// <summary>
/// 持続時間が切れたとき
/// </summary>
void OnLeave();
}
}
「 👆 DoIt
を、 OnEnter
に書き直すだけでも 一貫性が出てきそうだぜ」
「 コマンドに 開始時間と 持続時間 を持たせてしまえば どうだぜ?
名前を TimeSpan
にでも変えて、 コマンドはその特殊なケースにしろだぜ」
「 👆 TimedItem
と、 Command
と、 Movement
は、 TimeSpan
という枠組みで1本化したぜ」
📅 2023-02-01 sat 20:46 end
📅 2023-02-01 sat 21:02
「 👆 例えば PutDownCardOfHand
メソッドは、単に Movement
インスタンスを作るだけのメソッドだと分かった」
/// <summary>
/// 場札を並べる
///
/// - 左端は角度で言うと 112.0f
/// </summary>
internal void ArrangeHandCards(GameModel gameModel, int player, LazyArgs.SetValue<List<ISpan>> setSpans)
{ // ...
「 👆 例えば ArrangeHandCards
メソッドは、単に Movement
インスタンスを複数、作るだけのメソッドだと分かった」
📅 2023-02-01 sat 21:24
「 表現しづらいバグを起こすなあ。
持ち上げるカードを 間違えているんだぜ なぜか」
「 👆 タイム・スパンの インスタンスの中で 即実行 するコードがあると タイミングが狂ってる感じがするぜ」
「 OnEnter
メソッドの中ではなく、 Lerp
メソッドの中に書けば いいんじゃない?」
List<ISpan> SubSpans { get; set; }
// - メソッド
public override void Lerp(float progress)
{
base.Lerp(progress);
if (this.SubSpans!=null)
{
foreach (var span in this.SubSpans)
{
span.Lerp(progress);
}
}
}
「 コマンドのタイム・スパンの寿命と、
場札の位置調整をしたいタイム・スパンの寿命は 別物だからでは?」
「 じゃあ タイム・スパンを新しく スポーン(Spawn;生成)しなくちゃ いけないのよ」
「 👆 タイムライン・モデルは ゲーム・モデルの外側にあるし、 ゲーム・ビュー・モデルの外側でもあるぜ。
タイムライン・モデルは モデル なのだろうか? ビュー なのだろうか?」
「 モデルのくせに、ゲーム・オブジェクトを持ってるのは 良くない」
「 タイムライン・モデルの モデルとビューの分離 を先に行っておくべきだったんじゃない?」
「 👆 今なら Discard(ディスカード;更新の破棄)しても ダメージは30分レベルで軽微だから ロールバック(Rollback;巻き戻し)するかだぜ」
📅 2023-02-01 sat 21:49
「 👆 じゃあ Lerp
メソッドのような、ビュー に属するものを タイムライン・モデルが 持っていては
いけなくないかだぜ?」
「 TimelineView のようなものが 要るのかしら?」
📅 2023-02-02 sat 22:20
「 👆 OnEnter
メソッドは モデルを扱い、
Lerp
メソッドは ビューを扱うというように ぱっきり 分かれているので、
TimeSpan
も モデルとビューに分けられないかだぜ?」
Assets/Scripts/Models/IdOfCardSuits.cs
file:
namespace Assets.Scripts.Models
{
/// <summary>
/// カードのスート(絵柄)
/// </summary>
internal enum IdOfCardSuits
{
None,
Clubs,
Diamonds,
Hearts,
Spades,
}
}
Assets/Scripts/Models/IdOfCardSuits.cs
file:
namespace Assets.Scripts.Models
{
using System;
/// <summary>
/// トランプのカード
///
/// - ジョーカーを除く
/// </summary>
internal enum IdOfPlayingCards
{
// ... 中略 ...
}
static class IdOfPlayingCardsExtensions
{
public static IdOfCardSuits Suit(this IdOfPlayingCards idOfCard)
{
switch (idOfCard)
{
case IdOfPlayingCards.Clubs1:
case IdOfPlayingCards.Clubs2:
case IdOfPlayingCards.Clubs3:
case IdOfPlayingCards.Clubs4:
case IdOfPlayingCards.Clubs5:
case IdOfPlayingCards.Clubs6:
case IdOfPlayingCards.Clubs7:
case IdOfPlayingCards.Clubs8:
case IdOfPlayingCards.Clubs9:
case IdOfPlayingCards.Clubs10:
case IdOfPlayingCards.Clubs11:
case IdOfPlayingCards.Clubs12:
case IdOfPlayingCards.Clubs13:
return IdOfCardSuits.Clubs;
case IdOfPlayingCards.Diamonds1:
case IdOfPlayingCards.Diamonds2:
case IdOfPlayingCards.Diamonds3:
case IdOfPlayingCards.Diamonds4:
case IdOfPlayingCards.Diamonds5:
case IdOfPlayingCards.Diamonds6:
case IdOfPlayingCards.Diamonds7:
case IdOfPlayingCards.Diamonds8:
case IdOfPlayingCards.Diamonds9:
case IdOfPlayingCards.Diamonds10:
case IdOfPlayingCards.Diamonds11:
case IdOfPlayingCards.Diamonds12:
case IdOfPlayingCards.Diamonds13:
return IdOfCardSuits.Diamonds;
case IdOfPlayingCards.Hearts1:
case IdOfPlayingCards.Hearts2:
case IdOfPlayingCards.Hearts3:
case IdOfPlayingCards.Hearts4:
case IdOfPlayingCards.Hearts5:
case IdOfPlayingCards.Hearts6:
case IdOfPlayingCards.Hearts7:
case IdOfPlayingCards.Hearts8:
case IdOfPlayingCards.Hearts9:
case IdOfPlayingCards.Hearts10:
case IdOfPlayingCards.Hearts11:
case IdOfPlayingCards.Hearts12:
case IdOfPlayingCards.Hearts13:
return IdOfCardSuits.Hearts;
case IdOfPlayingCards.Spades1:
case IdOfPlayingCards.Spades2:
case IdOfPlayingCards.Spades3:
case IdOfPlayingCards.Spades4:
case IdOfPlayingCards.Spades5:
case IdOfPlayingCards.Spades6:
case IdOfPlayingCards.Spades7:
case IdOfPlayingCards.Spades8:
case IdOfPlayingCards.Spades9:
case IdOfPlayingCards.Spades10:
case IdOfPlayingCards.Spades11:
case IdOfPlayingCards.Spades12:
case IdOfPlayingCards.Spades13:
return IdOfCardSuits.Spades;
default: throw new ArgumentOutOfRangeException("idOfCard");
}
}
}
}
📅 2023-02-03 sat 00:12
📅 2023-02-03 sat 02:00
「 👆 タイム・ライン登録時は まだ 座標が動いてないから、
タイム・ライン登録中も 座標を動かしてやらないと いけないぜ。
この不具合は また今度直そう」
📺 開発中画面
📅 2023-02-04 sat 02:07
「 👆 7時間ぐらいバグ探して 1つ 直した。
操作を2連続で行うと カードが変なところに飛んでいくので、
2つの操作を結合して、1回の操作にする」
📺 開発中画面
「 作ってみると 分からないところが いっぱい出てくる。散々だぜ」
📅 2023-02-04 sat 13:53
「 👆 GameViewModel
を廃止し、静的クラスにしたぜ」
📅 2023-02-04 sat 19:43
📅 2023-02-04 sat 21:20
「 👆 5.0 だけ持ち上げたいのに、すでに 25.5 も持ち上がってたら、 6倍ぐらい 飛び上がるよな」
「 1.06、 2.17、 3.83、 6.06、 8.83、 12.17、 16.06、 20.50、 25.50 は、
+1.06、 +1.11、 +1.66、 +2.23、 +2.77、 +3.34、 +3.89、 +4.44、 +5.0 だぜ」
「 👆 説明するのは難しいが 理解した。
開始地点から 終了地点まで 刻んで動け、という命令をしてるときに
開始地点が 刻々と 進んでいるんだぜ」
「 👆 こういう書き方で 修正できたが、
こんな書き方が役に立つ場面 初めて見た。
不思議な気分だぜ」
📺 開発中画面
📅 2023-02-04 sat 23:55
「 👆 思ったのと 違う動きをしているが、
不具合のリクツが分かってきたのは前進だぜ」
📅 2023-02-05 sat 17:53
📺 開発中画面
📅 2023-02-05 sat 18:02
「 連打を禁止したら どうだぜ?
Lerp
が重なってるケースがあるのでは?」
📅 2023-02-05 sat 18:09
「 👆 入力系は こんがらがると 大変だろうから ゲーム・マネージャーと分離するぜ」
📅 2023-02-05 sat 18:10 end
📅 2023-02-06 mon 03:28
「 👆 やることの順番を 少しでも 前後間違えると 違った動きをする。
直した」
📺 開発中画面
📺 開発中画面
📅 2023-02-06 mon 04:13
📅 2023-02-06 mon 05:03
Assets/Scripts/Gui/GameManager.cs
file:
// - プロパティ
// モデル・バッファー
GameModelBuffer modelBuffer = new GameModelBuffer();
/// <summary>
/// ゲーム・モデル
/// </summary>
internal GameModel Model
{
get
{
if (model == null)
{
// ゲーム・モデルは、ゲーム・モデル・バッファーを持つ
model = new GameModel(modelBuffer);
}
return model;
}
}
GameModel model;
/// <summary>
/// スケジュール・レジスター
/// </summary>
internal ScheduleRegister ScheduleRegister
{
get
{
if (scheduleRegister == null)
{
// スケジューラー・レジスターは、ゲーム・モデルを持つ。
scheduleRegister = new TimedGeneratorOfSpanOfLearp.ScheduleRegister(this.Model);
}
return scheduleRegister;
}
}
ScheduleRegister scheduleRegister;
「 👆 プロパティは、ゲットしたタイミングで生成されるようにするぜ。
どのゲーム・オブジェクトの Start()
イベントハンドラから実行されるか 順番が不定なケースではこうする」
Assets/Scripts/Gui/InputManager.cs
file:
using Assets.Scripts.Gui.SpanOfLerp.TimedGenerator;
using Assets.Scripts.ThinkingEngine.CommandArgs;
using UnityEngine;
public class InputManager : MonoBehaviour
{
// - フィールド
ScheduleRegister scheduleRegister;
// - イベントハンドラ
// Start is called before the first frame update
void Start()
{
scheduleRegister = GameObject.Find("Game Manager").GetComponent<GameManager>().ScheduleRegister;
}
/// <summary>
/// Update is called once per frame
///
/// - 入力は、すぐに実行は、しません
/// - 入力は、コマンドに変換して、タイムラインへ登録します
/// </summary>
void Update()
{
const int right = 0;// 台札の右
const int left = 1;// 台札の左
bool handled1player = false;
bool handled2player = false;
// 先に登録したコマンドの方が早く実行される
// (ボタン押下が同時なら)右の台札は1プレイヤー優先
// ==================================================
if (Input.GetKeyDown(KeyCode.DownArrow))
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
scheduleRegister.AddJustNow(new MoveCardToCenterStackFromHandModel(
player: 0, // 1プレイヤーが
place: right)); // 右の
handled1player = true;
}
if (Input.GetKeyDown(KeyCode.W))
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
scheduleRegister.AddJustNow(new MoveCardToCenterStackFromHandModel(
player: 1, // 2プレイヤーが
place: right)); // 右の
handled2player = true;
}
// (ボタン押下が同時なら)左の台札は2プレイヤー優先
// ==================================================
// 2プレイヤー
if (Input.GetKeyDown(KeyCode.S))
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)左の台札へ積み上げる
scheduleRegister.AddJustNow(new MoveCardToCenterStackFromHandModel(
player: 1, // 2プレイヤーが
place: left)); // 左の
handled2player = true;
}
// 1プレイヤー
if (Input.GetKeyDown(KeyCode.UpArrow))
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)左の台札へ積み上げる
scheduleRegister.AddJustNow(new MoveCardToCenterStackFromHandModel(
player: 0, // 1プレイヤーが
place: left)); // 左の
handled1player = true;
}
// それ以外のキー入力は、同時でも勝敗に関係しない
// ==============================================
// 1プレイヤー
if (handled1player)
{
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
// 1プレイヤーのピックアップしているカードから見て、(1プレイヤーから見て)左隣のカードをピックアップするように変えます
scheduleRegister.AddJustNow(new MoveFocusToNextCardModel(
player: 0,
direction: 1));
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
// 1プレイヤーのピックアップしているカードから見て、(1プレイヤーから見て)右隣のカードをピックアップするように変えます
scheduleRegister.AddJustNow(new MoveFocusToNextCardModel(
player: 0,
direction: 0));
}
// 2プレイヤー
if (handled2player)
{
}
else if (Input.GetKeyDown(KeyCode.A))
{
// 2プレイヤーのピックアップしているカードから見て、(2プレイヤーから見て)左隣のカードをピックアップするように変えます
scheduleRegister.AddJustNow(new MoveFocusToNextCardModel(
player: 1,
direction: 1));
}
else if (Input.GetKeyDown(KeyCode.D))
{
// 2プレイヤーのピックアップしているカードから見て、(2プレイヤーから見て)右隣のカードをピックアップするように変えます
scheduleRegister.AddJustNow(new MoveFocusToNextCardModel(
player: 1,
direction: 0));
}
// デバッグ用
if (Input.GetKeyDown(KeyCode.Space))
{
// 両プレイヤーは手札から1枚抜いて、場札として置く
for (var player = 0; player < 2; player++)
{
// 場札を並べる
scheduleRegister.AddJustNow(new MoveCardsToHandFromPileModel(
player: player,
numberOfCards: 1));
}
}
}
}
📅 2023-02-07 tue 05:03
「 👆 入力は、入力されたらすぐ実行するということはせず、
入力時のゲーム時間を付けて、スケジュール・レジスターに コマンドを登録するぜ」
📅 2023-02-07 tue 05:04
「 👆 入力の中に、ゲームの制約を書き込むと 読みづらくなるので、
LegalManager
というのを別途作って、こっちに ゲームの制約を組み込んでいくぜ」
Assets/Scripts/ThinkingEngine/IdOfPlayingCards.cs
file:
namespace Assets.Scripts.ThinkingEngine
{
using System;
/// <summary>
/// トランプのカード
///
/// - ジョーカーを除く
/// </summary>
internal enum IdOfPlayingCards
{ // ...
}
static class IdOfPlayingCardsExtensions
{
public static IdOfCardSuits Suit(this IdOfPlayingCards idOfCard)
{ // ...
}
public static int Number(this IdOfPlayingCards idOfCard)
{
switch (idOfCard)
{
case IdOfPlayingCards.Clubs1:
case IdOfPlayingCards.Diamonds1:
case IdOfPlayingCards.Hearts1:
case IdOfPlayingCards.Spades1:
return 1;
case IdOfPlayingCards.Clubs2:
case IdOfPlayingCards.Diamonds2:
case IdOfPlayingCards.Hearts2:
case IdOfPlayingCards.Spades2:
return 2;
case IdOfPlayingCards.Clubs3:
case IdOfPlayingCards.Diamonds3:
case IdOfPlayingCards.Hearts3:
case IdOfPlayingCards.Spades3:
return 3;
case IdOfPlayingCards.Clubs4:
case IdOfPlayingCards.Diamonds4:
case IdOfPlayingCards.Hearts4:
case IdOfPlayingCards.Spades4:
return 4;
case IdOfPlayingCards.Clubs5:
case IdOfPlayingCards.Diamonds5:
case IdOfPlayingCards.Hearts5:
case IdOfPlayingCards.Spades5:
return 5;
case IdOfPlayingCards.Clubs6:
case IdOfPlayingCards.Diamonds6:
case IdOfPlayingCards.Hearts6:
case IdOfPlayingCards.Spades6:
return 6;
case IdOfPlayingCards.Clubs7:
case IdOfPlayingCards.Diamonds7:
case IdOfPlayingCards.Hearts7:
case IdOfPlayingCards.Spades7:
return 7;
case IdOfPlayingCards.Clubs8:
case IdOfPlayingCards.Diamonds8:
case IdOfPlayingCards.Hearts8:
case IdOfPlayingCards.Spades8:
return 8;
case IdOfPlayingCards.Clubs9:
case IdOfPlayingCards.Diamonds9:
case IdOfPlayingCards.Hearts9:
case IdOfPlayingCards.Spades9:
return 9;
case IdOfPlayingCards.Clubs10:
case IdOfPlayingCards.Diamonds10:
case IdOfPlayingCards.Hearts10:
case IdOfPlayingCards.Spades10:
return 10;
case IdOfPlayingCards.Clubs11:
case IdOfPlayingCards.Diamonds11:
case IdOfPlayingCards.Hearts11:
case IdOfPlayingCards.Spades11:
return 11;
case IdOfPlayingCards.Clubs12:
case IdOfPlayingCards.Diamonds12:
case IdOfPlayingCards.Hearts12:
case IdOfPlayingCards.Spades12:
return 12;
case IdOfPlayingCards.Clubs13:
case IdOfPlayingCards.Diamonds13:
case IdOfPlayingCards.Hearts13:
case IdOfPlayingCards.Spades13:
return 13;
default: throw new ArgumentOutOfRangeException("idOfCard");
}
}
}
}
📅 2023-02-07 tue 05:24
「 👆 トランプ・カードのIdを、数に変える方法が無かったので、作っておくぜ」
Assets/Scripts/ThinkingEngine/LegalMove.cs
file:
namespace Assets.Scripts.ThinkingEngine
{
internal class LegalMove
{
// - メソッド
internal static bool CanPutToCenterStack(GameModel gameModel, int player, int place)
{
int index = gameModel.GetIndexOfFocusedCardOfPlayer(player);
if (index == -1)
{
return false;
}
IdOfPlayingCards topCard = gameModel.GetLastCardOfCenterStack(place);
if (topCard == IdOfPlayingCards.None)
{
return false;
}
var numberOfPickup = gameModel.GetCardsOfPlayerHand(player)[index].Number();
int numberOfTopCard = topCard.Number();
// とりあえず差分を取る。
// 負数が出ると、負数の剰余はプログラムによって結果が異なるので、面倒だ。
// 割る数を先に足しておけば、剰余をしても負数にはならない
int divisor = 13; // 法
int remainder = (numberOfTopCard - numberOfPickup + divisor) % divisor;
return remainder == 1 || remainder == divisor - 1;
}
}
}
📅 2023-02-07 tue 06:10
「 👆 LegalManager
というゲーム・オブジェクトにアタッチする C#スクリプトは止めて、
LegalMove
という静的クラスを作ったぜ」
// (ボタン押下が同時なら)右の台札は1プレイヤー優先
// ==================================================
if (Input.GetKeyDown(KeyCode.DownArrow) && LegalMove.CanPutToCenterStack(
gameModel: scheduleRegister.GameModel,
player: 0, // 1プレイヤーが
place: right)) // 右の
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
scheduleRegister.AddJustNow(new MoveCardToCenterStackFromHandModel(
player: 0, // 1プレイヤーが
place: right)); // 右の
handled1player = true;
}
「 ついでに、ボタン連打をするという スパム行為 をやめさせたいな。
前に押してから 何秒経過するまで 次の入力ができないというような 制約を付ければいいのかだぜ?」
📺 作業用BGM
📅 2023-02-07 tue 07:03
「 👆 コマンドには 持続時間が自動的にセットされるようにして……」
「 すべての入力は コマンドに変換してから実行されるという 建付け にしておけば、
入力による 必要な待機時間は 決まるな」
// - フィールド
// ...
float[] spamSeconds = new[] { 0f, 0f };
// - イベントハンドラ
// ...
void Update()
{
// もう入力できないなら真
bool[] handled = { false, false };
for (var player = 0; player < 2; player++)
{
// 前判定
// もう入力できないなら真
handled[player] = 0 < spamSeconds[player];
// スパン時間消化
if (0 < spamSeconds[player])
{
// 負数になっても気にしない
spamSeconds[player] -= Time.deltaTime;
}
}
const int right = 0;// 台札の右
const int left = 1;// 台札の左
// 先に登録したコマンドの方が早く実行される
// (ボタン押下が同時なら)右の台札は1プレイヤー優先
// ==================================================
// 1プレイヤー
{
var player = 0;
if (!handled[player] && Input.GetKeyDown(KeyCode.DownArrow) && LegalMove.CanPutToCenterStack(
gameModel: scheduleRegister.GameModel,
player: player,
place: right)) // 右の
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveCardToCenterStackFromHandModel(
player: player, // 1プレイヤーが
place: right)); // 右の
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
handled[player] = true;
}
}
// ...
📅 2023-02-07 tue 08:15
「 👆 キー入力したら、コマンドに対応づく持続時間を覚えておいて、
その時間を消化しきるまで 次の入力ができないようにするぜ」
「 右、上、下、スペース を押してくれるだけで いいんじゃない?」
「 じゃあ ひとまず、
1P vs 2P、
1P vs COM、
COM vs 2P、
COM vs COM
のボタンを作るかだぜ?」
📅 2023-02-09 thu 18:54
「 👆 Shader を UI/Default
にすると半透明にできるの、ノーヒントでは 気づかないよな」
📅 2023-02-09 thu 18:57
📅 2023-02-09 thu 22:48
Assets/Scripts/Gui/InputManager/ToMeaning.cs
file:
namespace Assets.Scripts.Gui.InputManager
{
using UnityEngine;
/// <summary>
/// キー入力の解析
/// </summary>
internal class ToMeaning
{
// - プロパティ
/// <summary>
/// 自分に近い方の台札へ置く
/// </summary>
internal bool[] MoveCardToCenterStackNearMe { get; private set; } = new[] { false, false };
/// <summary>
/// 自分から遠い方の台札へ置く
/// </summary>
internal bool[] MoveCardToFarCenterStack { get; private set; } = new[] { false, false };
/// <summary>
/// 自分から見て(今ピックアップしているカードの)右隣のカードをピックアップ
/// </summary>
internal bool[] PickupCardToForward { get; private set; } = new[] { false, false };
/// <summary>
/// 自分から見て(今ピックアップしているカードの)左隣のカードをピックアップ
/// </summary>
internal bool[] PickupCardToBackward { get; private set; } = new[] { false, false };
/// <summary>
/// 手札から場札を補充する
/// </summary>
internal bool Drawing { get; private set; } = false;
// - メソッド
/// <summary>
/// 解析結果を全部消す
/// </summary>
internal void Clear()
{
for (var player = 0; player < 2; player++)
{
MoveCardToCenterStackNearMe[player] = false;
MoveCardToFarCenterStack[player] = false;
PickupCardToForward[player] = false;
PickupCardToBackward[player] = false;
}
Drawing = false;
}
/// <summary>
/// 物理的なキー入力を、意味的に置き換える
/// </summary>
/// <param name="player"></param>
internal void UpdateFromInput(int player)
{
if (player == 0)
{
MoveCardToCenterStackNearMe[player] = Input.GetKeyDown(KeyCode.DownArrow);
MoveCardToFarCenterStack[player] = Input.GetKeyDown(KeyCode.UpArrow);
PickupCardToForward[player] = Input.GetKeyDown(KeyCode.RightArrow);
PickupCardToBackward[player] = Input.GetKeyDown(KeyCode.LeftArrow);
}
else
{
MoveCardToCenterStackNearMe[player] = Input.GetKeyDown(KeyCode.S);
MoveCardToFarCenterStack[player] = Input.GetKeyDown(KeyCode.W);
PickupCardToForward[player] = Input.GetKeyDown(KeyCode.D);
PickupCardToBackward[player] = Input.GetKeyDown(KeyCode.A);
}
Drawing = Input.GetKeyDown(KeyCode.Space); // 1プレイヤーと、2プレイヤーの2回判定されてしまう
}
/// <summary>
/// 解析結果を全部上書きする
/// </summary>
internal void Overwrite(
int player,
bool moveCardToCenterStackNearMe,
bool moveCardToFarCenterStack,
bool pickupCardToForward,
bool pickupCardToBackward,
bool drawing)
{
MoveCardToCenterStackNearMe[player] = moveCardToCenterStackNearMe;
MoveCardToFarCenterStack[player] = moveCardToFarCenterStack;
PickupCardToForward[player] = pickupCardToForward;
PickupCardToBackward[player] = pickupCardToBackward;
Drawing = drawing;
}
}
}
「 👆 何キーを押したかではなく、どういう意図で押したかで データを持つクラスを作るぜ」
📅 2023-02-09 thu 22:51
Assets/Scripts/ThinkingEngine/Computer.cs
file:
namespace Assets.Scripts.ThinkingEngine
{
using Assets.Scripts.ThinkingEngine.Model;
/// <summary>
/// コンピューター・プレイヤー
/// </summary>
internal class Computer
{
// - その他
internal Computer(int number)
{
this.Number = number;
}
// - プロパティ
/// <summary>
/// プレイヤー番号
///
/// - 1プレイヤーなら0
/// </summary>
public int Number { get; private set; }
/// <summary>
/// 自分に近い方の台札へ置く
/// </summary>
internal bool MoveCardToCenterStackNearMe { get; private set; }
/// <summary>
/// 自分から遠い方の台札へ置く
/// </summary>
internal bool MoveCardToFarCenterStack { get; private set; }
/// <summary>
/// 自分から見て(今ピックアップしているカードの)右隣のカードをピックアップ
/// </summary>
internal bool PickupCardToForward { get; private set; }
/// <summary>
/// 自分から見て(今ピックアップしているカードの)左隣のカードをピックアップ
/// </summary>
internal bool PickupCardToBackward { get; private set; }
/// <summary>
/// 手札から場札を補充する
/// </summary>
internal bool Drawing { get; private set; }
// - メソッド
/// <summary>
/// コンピューター・プレイヤーが思考して、操作を決める
/// </summary>
/// <param name="gameModel">現在の局面</param>
internal void Think(GameModel gameModel)
{
// 今回の入力予定
var moveCardToCenterStackNearMe = false;
var moveCardToFarCenterStack = false;
var pickupCardToForward = false;
var pickupCardToBackward = false;
var drawing = false;
// 順繰りにやってるだけ
if (this.MoveCardToCenterStackNearMe == false && this.MoveCardToFarCenterStack == false && this.PickupCardToForward == false && this.Drawing == false)
{
moveCardToCenterStackNearMe = true;
}
else if (this.MoveCardToCenterStackNearMe)
{
moveCardToCenterStackNearMe = false;
moveCardToFarCenterStack = true;
}
else if (this.MoveCardToFarCenterStack)
{
moveCardToFarCenterStack = false;
pickupCardToForward = true;
}
else if (this.PickupCardToForward)
{
pickupCardToForward = false;
drawing = true;
}
else if (this.Drawing)
{
drawing = false;
moveCardToCenterStackNearMe = true;
}
// 今回の入力
this.MoveCardToCenterStackNearMe = moveCardToCenterStackNearMe;
this.MoveCardToFarCenterStack = moveCardToFarCenterStack;
this.PickupCardToForward = pickupCardToForward;
this.PickupCardToBackward = pickupCardToBackward;
this.Drawing = drawing;
}
}
}
📅 2023-02-09 thu 22:54
Assets/Scripts/Gui/InputManager.cs
file:
using GuiOfInputManager = Assets.Scripts.Gui.InputManager;
using Assets.Scripts.Gui.SpanOfLerp.TimedGenerator;
using Assets.Scripts.ThinkingEngine.Model;
using Assets.Scripts.ThinkingEngine.Model.CommandArgs;
using UnityEngine;
using GuiOfTimedCommandArgs = Assets.Scripts.Gui.TimedCommandArgs;
using Assets.Scripts.ThinkingEngine;
public class InputManager : MonoBehaviour
{
// - フィールド
ScheduleRegister scheduleRegister;
/// <summary>
/// コンピューター・プレイヤー用
/// </summary>
GameModel gameModel;
float[] spamSeconds = new[] { 0f, 0f };
/// <summary>
/// コンピューター・プレイヤーか?
///
/// - コンピューターなら Computer インスタンス
/// - コンピューターでなければヌル
/// </summary>
internal Computer[] Computers { get; set; } = new Computer[] { new Computer(0), new Computer(1), };
GuiOfInputManager.ToMeaning inputToMeaning = new GuiOfInputManager.ToMeaning();
// - イベントハンドラ
// Start is called before the first frame update
void Start()
{
var gameManager = GameObject.Find("Game Manager").GetComponent<GameManager>();
scheduleRegister = gameManager.ScheduleRegister;
gameModel = gameManager.Model;
}
/// <summary>
/// Update is called once per frame
///
/// - 入力は、すぐに実行は、しません
/// - 入力は、コマンドに変換して、タイムラインへ登録します
/// </summary>
void Update()
{
// キー入力の解析:クリアー
inputToMeaning.Clear();
// もう入力できないなら真
bool[] handled = { false, false };
for (var player = 0; player < 2; player++)
{
// 前判定:もう入力できないなら真
//
// - スパム中
// - 対局停止中
handled[player] = 0 < spamSeconds[player] || !gameModel.IsGameActive;
if (!handled[player])
{
if (Computers[player] == null)
{
// キー入力の解析:人間の入力を受付
inputToMeaning.UpdateFromInput(player);
}
else
{
// コンピューター・プレイヤーが思考して、操作を決める
Computers[player].Think(gameModel);
// キー入力の解析:コンピューターからの入力を受付
inputToMeaning.Overwrite(
player: player,
moveCardToCenterStackNearMe: Computers[player].MoveCardToCenterStackNearMe,
moveCardToFarCenterStack: Computers[player].MoveCardToFarCenterStack,
pickupCardToForward: Computers[player].PickupCardToForward,
pickupCardToBackward: Computers[player].PickupCardToBackward,
drawing: Computers[player].Drawing);
}
}
// スパン時間消化
if (0 < spamSeconds[player])
{
// 負数になっても気にしない
spamSeconds[player] -= Time.deltaTime;
}
}
const int right = 0;// 台札の右
const int left = 1;// 台札の左
// 先に登録したコマンドの方が早く実行される
// (ボタン押下が同時なら)右の台札は1プレイヤー優先
// ==================================================
// 1プレイヤー
{
var player = 0;
if (!handled[player] && inputToMeaning.MoveCardToCenterStackNearMe[player] && LegalMove.CanPutToCenterStack(
gameModel: scheduleRegister.GameModel,
player: player,
place: right)) // 右の
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveCardToCenterStackFromHandModel(
player: player, // 1プレイヤーが
place: right)); // 右の
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
handled[player] = true;
}
}
// 2プレイヤー
{
var player = 1;
if (!handled[player] && inputToMeaning.MoveCardToFarCenterStack[player] && LegalMove.CanPutToCenterStack(
gameModel: scheduleRegister.GameModel,
player: player,
place: right)) // 右の)
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)右の台札へ積み上げる
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveCardToCenterStackFromHandModel(
player: player, // 2プレイヤーが
place: right)); // 右の
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
handled[player] = true;
}
}
// (ボタン押下が同時なら)左の台札は2プレイヤー優先
// ==================================================
// 2プレイヤー
{
var player = 1;
if (!handled[player] && inputToMeaning.MoveCardToCenterStackNearMe[player] && LegalMove.CanPutToCenterStack(
gameModel: scheduleRegister.GameModel,
player: player,
place: left))
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)左の台札へ積み上げる
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveCardToCenterStackFromHandModel(
player: player, // 2プレイヤーが
place: left)); // 左の
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
handled[player] = true;
}
}
// 1プレイヤー
{
var player = 0;
if (!handled[player] && inputToMeaning.MoveCardToFarCenterStack[player] && LegalMove.CanPutToCenterStack(
gameModel: scheduleRegister.GameModel,
player: player,
place: left))
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、(1プレイヤーから見て)左の台札へ積み上げる
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveCardToCenterStackFromHandModel(
player: player, // 1プレイヤーが
place: left)); // 左の
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
handled[player] = true;
}
}
// それ以外のキー入力は、同時でも勝敗に関係しない
// ==============================================
// 1プレイヤー
{
var player = 0;
if (handled[player])
{
}
else if (inputToMeaning.PickupCardToBackward[player])
{
// 1プレイヤーのピックアップしているカードから見て、(1プレイヤーから見て)左隣のカードをピックアップするように変えます
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveFocusToNextCardModel(
player: player,
direction: 1));
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
}
else if (inputToMeaning.PickupCardToForward[player])
{
// 1プレイヤーのピックアップしているカードから見て、(1プレイヤーから見て)右隣のカードをピックアップするように変えます
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveFocusToNextCardModel(
player: player,
direction: 0));
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
}
}
// 2プレイヤー
{
var player = 1;
if (handled[player])
{
}
else if (inputToMeaning.PickupCardToBackward[player])
{
// 2プレイヤーのピックアップしているカードから見て、(2プレイヤーから見て)左隣のカードをピックアップするように変えます
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveFocusToNextCardModel(
player: player,
direction: 1));
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
}
else if (inputToMeaning.PickupCardToForward[player])
{
// 2プレイヤーのピックアップしているカードから見て、(2プレイヤーから見て)右隣のカードをピックアップするように変えます
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveFocusToNextCardModel(
player: player,
direction: 0));
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
}
}
// デバッグ用
if (inputToMeaning.Drawing)
{
// 両プレイヤーは手札から1枚抜いて、場札として置く
for (var player = 0; player < 2; player++)
{
// 場札を並べる
var timedCommandArg = new GuiOfTimedCommandArgs.Model(new MoveCardsToHandFromPileModel(
player: player,
numberOfCards: 1));
spamSeconds[player] = timedCommandArg.Duration;
scheduleRegister.AddJustNow(timedCommandArg);
}
}
}
}
「 👆 人間のキー入力も、コンピューターのキー入力も、
同じルーチンに合流するように しておくぜ」
📅 2023-02-09 thu 22:59
Assets/Scripts/Gui/GameManager.cs
file:
// ... 前略 ...
// 登録:ピックアップ場札を、台札へ積み上げる
{
{
// 1プレイヤーが、ピックアップ中の場札を抜いて、右の台札へ積み上げる
var player = 0;
var spanModel = new MoveCardToCenterStackFromHandModel(
player: player, // 1プレイヤーが
place: right); // 右の
this.ScheduleRegister.AddWithinScheduler(player, spanModel);
}
{
// 2プレイヤーが、ピックアップ中の場札を抜いて、左の台札へ積み上げる
var player = 1;
var spanModel = new MoveCardToCenterStackFromHandModel(
player: player, // 2プレイヤーが
place: left); // 左の;
this.ScheduleRegister.AddWithinScheduler(player, spanModel);
}
}
// 対局開始の合図
{
var spanModel = new SetGameActive(
isGameActive: true);
{
var player = 0; // どっちでもいいが、とりあえず、プレイヤー1に 合図を出させる
this.ScheduleRegister.AddWithinScheduler(player, spanModel);
}
{
var player = 1; // プレイヤー2も、間を合わせる
this.ScheduleRegister.AddScheduleSeconds(
player: player,
seconds: new GuiOfTimedCommandArgs.Model(spanModel).Duration);
}
}
// 以下、デモ・プレイを登録
// SetupDemo();
// OnTick を 1.0 秒後に呼び出し、以降は tickSeconds 秒毎に実行
InvokeRepeating(nameof(OnTick), 1.0f, tickSeconds);
}
「 👆 対局開始コマンド というのを追加した。
これがないと コンピューターが ゲーム起動直後から 台札置くのも すっとばして 場札をいじりだしてしまうぜ」
📺 開発中画面
「 1プレイヤーと 2プレイヤーのどちらも カードを置けなくなったら、どうすんだっけ?」
「 いっせー のー せっ! の掛け声で一緒に出すんじゃないの?」
「 👆 手札から補充する、手札がなければ 場札から補充する、とあるぜ」
「 じゃあ、どちらも置けなくなったら、以下のケースがあるわけだぜ」
「 手札が残っている方は、手札の頂上から1枚 自動的に 出す」
「 手札が残っていない方は、場札を1枚 手動的に選んで 出す」
「 どちらかが カード出したら、残っている方は、3秒後に ピックアップしてる場札 勝手に出す、ということで いいのでは?」
「 じゃあ、置く札がない状況、仮に ステイルメイト(Stalemate) とでも呼ぶとして、
Stalemate かどうか判断する関数を1つ作ろうぜ?」
📅 2023-02-12 sun 18:07 ↑
「 👆 その前にこの 画面サイズに関係なく ボタンサイズがある、という
GUI なんとかならないの?」
「 Canvas Scaler コンポーネントというものがあるらしい。なんだぜそれ?」
📅 2023-02-12 sun 18:35
📅 2023-02-12 sun 18:39
「 👆 Scale with Screen Size
にしたら いいのかな?」
📅 2023-02-12 sun 18:42
📅 2023-02-12 sun 21:11
「 👆 大まかに、スクリプトを ThinkingEngine
と Vision
に分けることにする。
ThinkingEngine
というのは 画面なしで動くようなもので、
Vision
というのは 画面で動くようなゲームが入ってるところだぜ」
「 その Vision
の中は、さらに3つに分けることにする。
Input
というのは キーボードからの入力だな。
UserInterface
というのは、画面上のボタンとかだぜ。
World
は、まあ その他 ぐらいに思えだぜ」
「 👆 今までは 勝手にゲームが始まっていたが、
StartGame()
メソッドを呼び出すまで 始まらないように変更する」
「 👆 じゃあ どこで始まるの? というと、
ボタンを押したときに 始まるようにする。
これらは Assets.Scripts.Vision.UserInterface.Manager
クラスにまとめてある」
「 👆 ボタンを押すと On Click() のリストに登録したメソッドが実行される仕組みは 覚えておいてくれだぜ」
📺 開発中画面
📅 2023-02-12 sun 21:36 end
「 おっ、1か月ぶりか。
Unity の画面を見るのも嫌になってるが、
既知の不具合を1つ 直そうぜ?」
「 👆 入力したことを そのまま実行されると
おかしくなることがあるので、制約を付けたいんだぜ」
「 👆 それより ステールメート(※置けるカードがない)したら どうすんの?」
「 カードを引きたくても、手札(自分の側に積んでいたカード)も 無くなってるぜ」
「 👆 せーの、で 場札の好きなカードを1枚 捨てれるみたいだな」
「 今、ステールメートしてるかどうか判定するアルゴリズムを書いてくれだぜ」
「 気分乗らないけど、
ステールメートしてるかどうか 判定するアルゴリズムを考えるか……」
入力:
1P の場札
2P の場札
右の台札
左の台札
出力:
Yes/No
置ける数の配列=[
(右札の台札の数+1ー1) mod 13,
(右札の台札の数ー1ー1) mod 13,
(左札の台札の数+1ー1) mod 13,
(左札の台札の数ー1ー1) mod 13]
「 👆 最小2つ~最大4つの数を、プレイヤーのどっちかが持ってれば 結果は No だぜ」
「 1Pの場札、2Pの場札を 愚直に調べるしかないかだぜ?」
Assets/Scripts/ThinkingEngine/Model/LegalMove.cs
:
namespace Assets.Scripts.ThinkingEngine.Model
{
internal class LegalMove
{
// - メソッド
internal static bool CanPutToCenterStack(GameModel gameModel, int player, int place)
{
int index = gameModel.GetIndexOfFocusedCardOfPlayer(player);
if (index == -1)
{
return false;
}
IdOfPlayingCards topCard = gameModel.GetLastCardOfCenterStack(place);
if (topCard == IdOfPlayingCards.None)
{
return false;
}
var numberOfPickup = gameModel.GetCardsOfPlayerHand(player)[index].Number();
int numberOfTopCard = topCard.Number();
// とりあえず差分を取る。
// 負数が出ると、負数の剰余はプログラムによって結果が異なるので、面倒だ。
// 割る数を先に足しておけば、剰余をしても負数にはならない
int divisor = 13; // 法
int remainder = (numberOfTopCard - numberOfPickup + divisor) % divisor;
return remainder == 1 || remainder == divisor - 1;
}
}
}
「 👆 台札に置けるか、というメソッドをもう作ってあった。これを使えばいいだけかも」
「 👆 ステールメートの判定は、 インプット・マネージャーに書けばいいかな」
「 👆 TODO
コメントを使って 日本語でプログラムのプレースホルダーを書いていくぜ」
📅 2023-03-18 sat 13:39
「 👆 既存のコードと同じものを2回書くぐらいなら、共通化するぜ」
「 カウント・ダウン・タイマーという 新しい要素を 追加しないといけないわよ?」
「 開始と終了の位置を線形補間する Lerp
はあるけど、
カウントダウンして 時間が来たら 強制的にカードを台札に置くような
仕掛けは 作ってないんだが」
「 カウントダウンが インプット・マネージャーの仕事とは思わん。
ユーザー・インターフェース・マネージャーに カウントダウンをやらせて、
インプット・マネーシャーから ユーザー・インターフェース・マネージャーへ依頼する形にしようかな」
「 お互いにおけるカードがなくなったから、カードを置き直すの、名前 何かあるのかだぜ?」
「 分かんないから 再開(Reopening)でいいんじゃないの?」
📖 【Unity基礎】コルーチンの使い方と注意点まとめ徹底解説
「 あれ、ステールメート判定 できてないぜ。
持ってるカードだけ見て ステールメートを判定しちゃいけないんだ、
場札を全部見ないといけないんだ」
Assets.Scripts.ThinkingEngine.Models.Player.cs
:
namespace Assets.Scripts.ThinkingEngine.Models
{
/// <summary>
/// プレイヤーの配列の添え字
///
/// - プレイヤー1 は 0
/// - プレイヤー2 は 1
/// </summary>
class Player
{
// - 演算子のオーバーロード
#region 演算子のオーバーロード(== と !=)
// 📖 [自作クラスの演算子をオーバーロードする](https://dobon.net/vb/dotnet/beginner/operator.html)
// 📖 [自作クラスのEqualsメソッドをオーバーライドして、等価の定義を変更する](https://dobon.net/vb/dotnet/beginner/equals.html)
public static bool operator ==(Player c1, Player c2)
{
// nullの確認(構造体のようにNULLにならない型では不要)
// 両方nullか(参照元が同じか)
// (c1 == c2)とすると、無限ループ
if (object.ReferenceEquals(c1, c2))
{
return true;
}
// どちらかがnullか
// (c1 == null)とすると、無限ループ
if (((object)c1 == null) || ((object)c2 == null))
{
return false;
}
return (c1.source == c2.source) && (c1.source == c2.source);
}
public static bool operator !=(Player c1, Player c2)
{
// (c1 != c2)とすると、無限ループ
return !(c1 == c2);
}
/// <summary>
/// objと自分自身が等価のときはtrueを返す
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
//objがnullか、型が違うときは、等価でない
if (obj == null || this.GetType() != obj.GetType())
{
return false;
}
//この型が継承できないクラスや構造体であれば、次のようにできる
//if (!(obj is TestClass))
//Numberで比較する
Player c = (Player)obj;
return (this.source == c.source);
//または、
//return (this.Number.Equals(c.Number));
}
//Equalsがtrueを返すときに同じ値を返す
public override int GetHashCode()
{
return this.source;
}
#endregion
// - その他
internal Player(int source)
{
this.source = source;
}
// - フィールド
/// <summary>
/// 値
/// </summary>
int source;
// - プロパティー
/// <summary>
/// 整数型形式で取得
/// </summary>
internal int AsInt => source;
}
}
「 👆 プレイヤーを int 型で表すのは 辛くなってきたので
クラスを使って Player型 を作るぜ。
C# に typedef は無い」
Assets.Scripts.ThinkingEngine.Commons.cs
:
namespace Assets.Scripts.ThinkingEngine
{
using Assets.Scripts.ThinkingEngine.Models;
/// <summary>
/// よく使う不変値
/// </summary>
static class Commons
{
internal static readonly Player Player1 = new Player(0);
internal static readonly Player Player2 = new Player(1);
internal static readonly Player[] Players = new Player[]
{
Player1,
Player2,
};
}
}
Assets/Scripts/Vision/Input/Manager.cs
:
// ステールメートしてるかどうかの判定
// ==================================
// ステールメートしているとき
bool isStalemate = true;
// 反例を探す
foreach (var playerObj in Commons.Players)
{
foreach (var centerStackPlace in Commons.CenterStacks)
{
var max = this.gameModel.GetCardsOfPlayerHand(playerObj).Count;
for (int i = 0; i < max; i++)
{
if (LegalMove.CanPutToCenterStack(
this.gameModel,
playerObj,
new HandCardIndex(i),
centerStackPlace))
{
isStalemate = false;
goto end_loop;
}
}
}
}
end_loop:
if (isStalemate)
{
// TODO ★ カウントダウン・タイマーを表示。0になったら、ピックアップ中の場札を強制的に台札へ置く
this.reopeningManager.DoIt();
}
「 👆 コードの掲載は 省いていくが、雰囲気は こんな感じ」
📺 開発中画面
📅 2023-03-18 sat 23:49
「 👆 そのあと 大改造して カウントダウンは 付けたぜ。
今日はここまで」
「 Unity の顔を見るのも嫌になっているが 不具合の調査をするかだぜ」
「 ゲームの結果を表示してくれだぜ。
1Pの勝ち、2Pの勝ち、引き分け のいずれかだろ」
「 両プレイヤーが 同時に最後のカードを 捨てるの、
何ミリ秒差まで 厳密に判定するの?
一瞬でも速く置いた方が 勝ちなの?」
「 レトロな 格闘ゲーマーは 1フレームの差を 文句言ってくるからな」
「 じゃあ 1フレームの差を区別しろだぜ。
両者が 最後の1枚を残して ステールメートしてるときは 同時にカード捨てるから 引き分けになるぜ」
「 カードを捨てた瞬間か、
それとも カードを台札に置いた瞬間か、
どっちで判定するんだぜ?」
「 物理演算を使うと、距離とか 速度とか 公平性 保たなくてはいけなくなって大変なので
デジタルな判定にしたいぜ」
「 台札に投げたカードの モーションの終点で フラグ立てるか」
「 これから 風呂入って ビール飲んで まだやる気があったら 開発するぜ」
「 はあ~ Unity の顔も見たくねー、起動だけ するか~」
「 やる気がない時期を やりすごすのが 長く続けるには 重要なのよ」
📅 2023-03-23 thu 23:11
「 待ってくれだぜ つら。
ジュース飲んで 気分が上がってくれば プログラミングするぜ」
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント