【実況ブログ】 Unity でスピードを作ろうぜ(^~^)?

読了目安:140分

📅2023-01-23 mon 21:00

202101__character__31--ramen-tabero-futsu2.png
「 Unity で スピードのモックアップを作ろうぜ?」

202101__character__28--kifuwarabe-futsu.png
「 お父ん、モックアップしか作らないから つまんな……」

202108__character__12--ohkina-hiyoko-futsu2.png
「 企画書と 設計書のフェーズは すっ飛ばすんでしょ。
プログラミングしかやらないから」

202301_unity_23-2113--unity-hub-1.png
202101__character__31--ramen-tabero-futsu2.png
「 👆 Unity Hub でプロジェクトを作る所から始まるぜ」

202301_unity_23-2116--new-project-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 プロジェクト名は Speed でいいだろ」

202301_unity_23-2122--files.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 ファイルが最初から、いくつか 入ってるぜ」

202301_unity_23-2124--unity-editor.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 Unity Editor が出てくるが、自分が使いやすいように セットアップしておいたぜ。
ほんとは ディスプレイいっぱい でかく広げて作業しているが、
上の画像は ブログにアップするために ウィンドウを小さくしているぜ」

202101__character__28--kifuwarabe-futsu.png
「 その ディスプレイいっぱいに広げたウィンドウの画像も 1回 見せてくれだぜ」

202301_unity_23-2127--maximized-window.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうだぜ」

202301_unity_23-2129--images-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 トランプ・ゲーム作るんだから トランプの画像がいるだろ。
Project ウィンドウの Assets フォルダーの下に Images フォルダーを作って、
右クリックして Show in Explorer をクリックしろだぜ」

202301_unity_23-2137--playing-cards.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 前に神経衰弱を作った時に 描いたものを フォルダーへぶち込むぜ」

202301_unity_23-2139--assets-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 Unity Editor からも見えるな」

202101__character__28--kifuwarabe-futsu.png
「 しかし また 1枚1枚 プレーンを置いて 画像をプレーンにドラッグ&ドロップ していくのかだぜ?」

202101__character__31--ramen-tabero-futsu2.png
「 👆 それは きついな……。どないしよ……」

202108__character__12--ohkina-hiyoko-futsu2.png
「 前に作った Concentration (コンセントレーション;トランプの神経衰弱ゲーム) を Inport Package (インポート・パッケージ)したらいいんじゃないの?」

202101__character__31--ramen-tabero-futsu2.png
「 やってみるか……」

📅2023-01-23 mon 21:44

202301_unity_23-2145--import-package-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 前に Import Package したら 中身をぶちまけられたり、ぐちゃぐちゃに壊されたりしたから 嫌なんだが
まだ プロジェクトを作ったばかりだし 被害もないだろ」

202301_unity_23-2152--file-chooser-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 開くぜ」

202301_unity_23-2154--import-unity-package.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 コンフリクトは注意してくれるのか」

202101__character__31--ramen-tabero-futsu2.png
「 あっ、 カードの ゲーム・オブジェクト が入ってないぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 Concentration プロジェクトの方で カードのゲーム・オブジェクトを プレファブにして Assets に入れておけばいいんじゃないの?」
202301_unity_23-2200--concentration-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 Hierarchy ウィンドウにあるだけでは Assets ではないから、 Assets に入れないといけないのか。
下準備が けっこう居るな。
元のプロジェクトを壊さずに ゲーム・オブジェクトを プレファブに差し替えられるかな?」

202301_unity_23-2204--prefabs-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 Hierarchy ウィンドウから Project ウィンドウへ ドラッグ&ドロップで コピーすることは でけるみたいだけど」

202101__character__28--kifuwarabe-futsu.png
「 じゃあ Hierarchy ウィンドウにある方の Hearts 1 フォルダーを消して、
Project ウィンドウにある方の Hearts 1 プレファブを Hierarchy ウィンドウに戻してみろだぜ」

202301_unity_23-2208--back-to-the-hierarchy-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 何ごともなく 無事 でけたぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 ふーん できるんだ」

202101__character__28--kifuwarabe-futsu.png
「 じゃあ 残り53枚のカードを プレファブに変換しろだぜ」

202101__character__31--ramen-tabero-futsu2.png
「 つら…… ぢごくだ……」

📅2023-01-23 mon 22:11

202301_unity_23-2212--highlight-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうやって選択して 一度に持っていけないかな……」

202301_unity_23-2215--prefabs-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 おっ、いけたようだぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 Hierarchy ウィンドウの方の 元のゲーム・オブジェクトも 水色のアイコンに変わってるわよ。
もう プレファブになってんじゃない?」

202301_unity_23-2219--export-package-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 なんでもいいや…… Export Package しよ」

202301_unity_23-2223--import-package-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 Speed プロジェクトの方で Import Package しよ」

202301_unity_23-2226--not-found-image-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 プレファブだけ持ってきてもだけで 画像も持ってこないと リンク切れを起こすか
当たり前と言えば 当たり前だが」

202301_unity_23-2230--images-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 画像を持ってきたら 復元した」

202101__character__28--kifuwarabe-futsu.png
「 スクリプトのアタッチが切れているようだぜ?」

202301_unity_23-2233--scripts-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 スクリプトを持ってきたら 復元したぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 トランプ・カードだけを インポート・パッケージしやすいような
トランプ・カードだけのプロジェクトを 作っておくべきなんじゃない?」」

202101__character__31--ramen-tabero-futsu2.png
「 下ごしらえか。 トランプ・ゲームをよく作るようなら 作っておいた方が良さそうだな」

📅2023-01-23 mon 22:36

202301_unity_23-2300--backward.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 なんか知らんけど 裏側 剥がれてるから 貼り直しだ ひ~」

202108__character__12--ohkina-hiyoko-futsu2.png
「 検品って 大事ね~」

202301_unity_23-2310--overrides-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 Unity Learn のビギナーコースで学んだところによると、 PrefabHierarchy でまた いじったら、 Inspector ウィンドウの Overrides ドロップダウンリストから
Apply All ボタンを選んで 押せば プレファブの設定を上書きしてくれるんだったと思う、多分」

202101__character__28--kifuwarabe-futsu.png
「 じゃあ 全部のカードの Apply All ボタンを押すのが終わったら、
Hierarchy ウィンドウのカードを全部消して、
Project ウィンドウにあるカードを Hierarchy ウィンドウへ ドラッグ&ドロップしろだぜ」

202101__character__31--ramen-tabero-futsu2.png
「 でけた。 今度は オモテも ウラも 画像が貼り付いてるぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 カードができたんだったら、 並べて、
スピードをやってるみたいな 画面を作りなさいよ」

202101__character__28--kifuwarabe-futsu.png
「 画作り(えづくり)か」

202301_unity_23-2323--picture-making-rotation-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 例えば 赤色のスートのカードだけ 180°回転させるとか Unity Editor を使って操作する。
こういう道具の使い方の基本操作が すばやいことが 開発屋の 基本のき だぜ」

202101__character__28--kifuwarabe-futsu.png
「 解説はいいから 早よ 画を作れだぜ」

202301_unity_23-2345--failed-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 あっ! カードを裏返そうとしたら オモテ面と ウラ面の両方が ウラの方向いて
ワケが分からなくなった!」

202101__character__28--kifuwarabe-futsu.png
「 やり直せ!」

202301_unity_23-2349--redo-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 不思議な話だが プロジェクトにある カードを全部消して、
エクスポートした自分自身の中身を 再び 自分に入れ直すぜ」

202301_unity_24-0003--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 オモテ面、ウラ面の2枚で1つのカードを ひっくり返す うまい操作が よく分からん。 疲れた。
今日は ここまでだぜ」

202101__character__28--kifuwarabe-futsu.png
「 おつ」

📅2023-01-24 tue 00:05 end

📅2023-01-24 tue 20:20 start

202301_unity_24-2021--turn-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 Speades 3 ゲーム・オブジェクトを選択して Rotation Z180 にすれば
オモテ面、ウラ面を1つのまとまりとして 裏返してくれるぜ」

202101__character__28--kifuwarabe-futsu.png
「 1個 1個 ゲーム・オブジェクトを選んで テキストボックスに 180 を入れていけだぜ」

202301_unity_24-2037--turn-and-pop-1.png

202101__character__31--ramen-tabero-futsu2.png
「 この カードを1枚ずつ選んで 裏返して 少し持ち上げる たこ焼き みたいな作業をやるの
嫌なんだが」

202108__character__12--ohkina-hiyoko-futsu2.png
「 もっと スピーディーにやる方法 あるんじゃないの?」

202301_unity_24-2053--move-1.png

202101__character__31--ramen-tabero-futsu2.png
「 あったとしても 知りようがないぜ。
ここを プログラミング化できたら 30分は 縮まる!」

202301_unity_24-2055--turn-and-pop-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 反対側にもあって まだ15枚もある!」

202101__character__28--kifuwarabe-futsu.png
「 30分 がんばれよ」

202301_unity_24-2110--picture-making-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 見た目は こんなんで いいかあ」

📅2023-01-24 tue 21:13

202108__character__12--ohkina-hiyoko-futsu2.png
「 手札って 最大で 何枚になるの?」

202101__character__31--ramen-tabero-futsu2.png
「 パイルを全部 手札に混ぜれば 20枚だぜ」

202101__character__28--kifuwarabe-futsu.png
「 20枚の手札があるケースも 画作り してくれだぜ」

202301_unity_24-2119--line-1.png

202101__character__31--ramen-tabero-futsu2.png
「 カードの端を ちょっと被せつつ並べれば 20枚は 収まるかな?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 WebGL でビルドして 出力結果で 確かめなさいよ」

202301_unity_24-2130--output.png

202101__character__31--ramen-tabero-futsu2.png
「 端が切れてるなあ。 カメラの角度が付いてるからかな?」

202101__character__28--kifuwarabe-futsu.png
「 メインカメラの位置を きっちり 決めてくれだぜ」

202301_unity_24-2155--main-camera-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 真上というのも 味気ないんで ちょっと傾けつつ 整数にまとめたぜ」

202301_unity_24-2154--output.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 画作り は こんなもんでいいだろ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 相手の手札を 前後ひっくり返す意味ってある?」

202101__character__28--kifuwarabe-futsu.png
「 ダイヤの6と、ダイヤの9は 見分けが付かないよな」

202101__character__31--ramen-tabero-futsu2.png
「 将棋の駒だって 後手は ひっくり返ってるだろ 例はある 気にするなだぜ」

📅2023-01-24 tue 22:01

202108__character__12--ohkina-hiyoko-futsu2.png
「 ゲーム開始時に セット されるプログラムを組みましょうよ」

202301_unity_24-2203--coordinate-1.png

202101__character__31--ramen-tabero-futsu2.png
「 ゲームを作るときは 座標をメモって置くのが コツだぜ」

202101__character__28--kifuwarabe-futsu.png
「 xとzの正負が逆じゃないか? カメラが裏向いてんじゃないか?」

202101__character__31--ramen-tabero-futsu2.png
「 あれまっ! ほんとだぜ……」

202301_unity_24-2217--flip-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 グローバル座標にして Y軸を回転軸にして 180°回転したぜ」

202301_unity_24-2219--coordinate-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 座標の目安は これでいいだろ」

📅2023-01-24 tue 22:28

202101__character__28--kifuwarabe-futsu.png
「 手札は 1枚~20枚 を 位置調整することになるだろ。
予め 計算式を まとめてくれだぜ」

202101__character__31--ramen-tabero-futsu2.png
「 カード1枚の横幅は だいたい 10 のようだぜ。それを元に計算してみるか」

202301_unity_24-2240--coordinate-calc.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 横幅が無限にあるのなら、 (カードの枚数 - 1) * -5 の位置から +10 間隔でカードを並べるだけでいいが……」

202301_unity_24-2248--coordinate-calc-b.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 左端が x=-62 、右端が x=62 と決まってて、カードは20枚あるのだった」

202101__character__31--ramen-tabero-futsu2.png
「 62 - (-62) = 124 なので、
横幅 124 の中に 20 枚のカードがあるので、
言い換えると
横幅 124 の中に 19 箇所のカードの隙間があるので、
1つの間隔は 124 / 19 = 6.526... んー すっきりしないなあ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 123.5 なら 19 で割り切れたのにね」

202101__character__28--kifuwarabe-futsu.png
「 割り切ろうとするなだぜ」

左端のカードの位置 + (左から何枚目 - 1) * ((右端のカードの位置 - 左端のカードの位置) / (カードの枚数 - 1))

Example:
    -62 + (左から何枚目 - 1) * (124/19)

202101__character__31--ramen-tabero-futsu2.png
「 👆 上式で x 座標はイケるだろう」

202101__character__28--kifuwarabe-futsu.png
「 じゃあ 座標の設計は 終わりだぜ」

📅2023-01-24 tue 23:02

202108__character__12--ohkina-hiyoko-futsu2.png
「 カードを配るのって どうやってプログラミングすんの?」

202301_unity_24-2313--arrangement.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 かき集めて シャッフルして 配るだけだぜ」

📅2023-01-24 tue 23:25

202101__character__28--kifuwarabe-futsu.png
「 ちょっと やってみてくれだぜ」

202301_unity_24-2326--game-manager-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 ゲーム・マネージャーを作る手順は 前にやったから 途中は省略するぜ」

202301_unity_24-2344--script-1.png

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()
    {

    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 まず、カードをかき集めて ざらっと 机に並べるコードを書いてみよう」

202301_unity_24-2346--game.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 カードが被ってしまって 数字が なんにも見えないが、
まあ、かき集めるのは でけたな」

202101__character__31--ramen-tabero-futsu2.png
「 続きはまた今度だぜ」

202101__character__28--kifuwarabe-futsu.png
「 おつ」

📅2023-01-24 tue 23:47

📅2023-01-25 mon 19:05

202108__character__12--ohkina-hiyoko-futsu2.png
「 数字が見えるようにしなさいよ」

202101__character__31--ramen-tabero-futsu2.png
「 用事のため あとで」

📅2023-01-25 mon 19:14

📅2023-01-27 fri 20:00

202101__character__31--ramen-tabero-futsu2.png
「 Excel でチューリング・マシン作ってたら時間が飛んだぜ。
戻ってきたぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 数字が見えるようにしなさいよ」

202301_unity_25-1908--screen-size.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 画面の座標は こんな感じか」

202101__character__31--ramen-tabero-futsu2.png
「 あっ、しまった!」

202301_unity_27-2127--game.png

202101__character__31--ramen-tabero-futsu2.png
「 うっかり ゲームの配置にしてしまった!」

202101__character__28--kifuwarabe-futsu.png
「 カードのテクスチャーや枚数を検品しないと 不良品が混じってるかもしれないのに……」

202108__character__12--ohkina-hiyoko-futsu2.png
「 じゃあ 先に進みましょう。
積みあがってる手札を 裏返しなさいよ」

📅2023-01-27 fri 21:31

202301_unity_27-2144--y-1.png

202101__character__31--ramen-tabero-futsu2.png
「 あれっ! まだ 時計回りに180°回転してないのに ひっくり返ってるぜ!」

202101__character__28--kifuwarabe-futsu.png
「 じゃあ 180°回転して 戻せだぜ」

202301_unity_27-2151--game.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 なんか積んでる手札の底が1枚ずれてるな。
まあいいや 進んでる間に 原因が見つかるだろ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 じゃあ 先に進みましょう。
場にオープンしているカードを1枚選んで ルールを気にせず 中央の台札に 積みましょう!」

📅2023-01-27 fri 21:53

202301_unity_27-2159--board.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 その前に カードが 空飛んでるのが気になるぜ 地面を置こう」

202301_unity_27-2205--board-on-game.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 位置は こんなもんでいいだろ」

202101__character__28--kifuwarabe-futsu.png
「 UI はどうすんだぜ? どのカードを選んでるとか」

202301_unity_27-2218--lift.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 カードを持ち上げてみると……、
カメラアングルと 光源の関係なのか 手前と奥のプレイヤーで 持ち上げた高さが違って見えるぜ」

202101__character__28--kifuwarabe-futsu.png
「 カメラの位置を調整したらどうだぜ?」

202301_unity_27-2227--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こつこつ 画作り……」

202301_unity_27-2231--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 回転も付けた方が カードゲームっぽいかな?」

    /// <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);
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 付けた変化を 関数にしておくぜ」

202101__character__28--kifuwarabe-futsu.png
「 行列にした方がよくないかだぜ? 逆関数 作るの めんどくさいだろ?」

202101__character__31--ramen-tabero-futsu2.png
「 そういう最適化は 問題点が出尽くして 完成したあとに やりたかったら やればいいんで」

202301_unity_27-2238--focus.png

    /// <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);
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 逆関数をあてれば 持ち上げたカードは場に戻り、
また別のカードを持ち上げれば カードを選んでいる雰囲気が出るな」

📅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);
            }
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 画作り をしている間は ベタ書きするぜ」

202101__character__31--ramen-tabero-futsu2.png
「 ただ、コードが長くなって 読みにくいよな」

202301_unity_28-1735--lazy-args-1.png

202301_unity_27-2259--policy-1.png

Assets.Scripts.LazyArgs.cs:

namespace Assets.Scripts
{
    /// <summary>
    /// コーディングのテクニックのための仕込み
    /// </summary>
    internal class LazyArgs
    {
        public delegate void SetValue<T>(T value);
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 そこで コードを短く書けるための 仕込み をするぜ」

202301_unity_28-1740--get-card-1.png

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);
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 カードを取得するという ありきたりな関数を書き」

202301_unity_27-2305--lambda-1.png

        // 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));

202101__character__31--ramen-tabero-futsu2.png
「 👆 長ったらしかったコードを ワンライナー(1行)で書けるようにしたぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 1プレイヤーが 0 で、 1枚目が 0 って分かりづらくない?」

202101__character__31--ramen-tabero-futsu2.png
「 序数と基数の違いだぜ 別のものなのだから仕方ない 慣れろだぜ」

📅2023-01-27 fri 23:09

202108__character__12--ohkina-hiyoko-futsu2.png
「 台札に1枚乗せなさいよ」

        // 右の台札を積み上げる
        {
            float x = rightCenterStackX;
            float y = minY;
            float z = rightCenterStackZ;
            foreach (var goCard in goCenterStacksCards[0])
            {
                SetPosRot(goCard, x, y, z);
                y += 0.2f;
            }
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 このコードだと 使い回しづらいので、使い回しやすい形にするかだぜ」

        // 右の台札を積み上げる
        {
            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;
            }
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 Y座標は 記憶することにしよう」

202301_unity_27-2332--hand-card-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 『台札は、場札から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;
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 左の台札と、右の台札を 別にした方がいいな。
カードの移動があったときに、同時に ポリゴンの位置と角度も設定しよう」

📅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;
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 一本化すれば こんな感じか」

        // 左の台札が空っぽの状態
        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 // 右の
                );
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 ゲーム開始時の台札は こんな感じに置く」

202301_unity_28-0013--coroutine-1.png

    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);
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 あっ、そうだ コルーチン使お」

202301_unity_28-0016--demo-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 台札に置けるぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 次は、手札から 1枚取ってきて 場札として置く動作を作りなさいよ。
そのとき 場札の位置が 歯抜けだったりするだろうから、位置の再調整がいるかもしれないわね」

202101__character__31--ramen-tabero-futsu2.png
「 今日はここまでだぜ」

202101__character__28--kifuwarabe-futsu.png
「 おつ」

📅2023-01-28 fri 00:20 end

📅2023-01-28 sat 17:51

202101__character__31--ramen-tabero-futsu2.png
「 👇 場札を並べるコードは 以下のように書いているんだが……」

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;
            }
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 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;

202101__character__31--ramen-tabero-futsu2.png
「 👆 その前に 決まりきった座標を 読取専用でメソッドからアクセスできる自由変数にしておこうぜ」

    /// <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;
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 プログラマーのやってることって、違う書き方のコードを1本化することだよな。
この2つを 1本化しようぜ?」

202301_unity_28-1830--player-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 プレイヤー1と2で 違う変数を使っているのを止めて、配列かリストにしようぜ?」

    readonly float[] handCardsZ = new[] { -28.0f, 42.0f };
    readonly float[] pileCardsZ = new[] { -12.0f, 26.0f };

202101__character__31--ramen-tabero-futsu2.png
「 👆 こう」

    /// <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;
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 1本化したら こうなったぜ」

📅2023-01-28 sat 18:46

202301_unity_28-1850--addPile-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 次は 『手札から1枚抜いて場札に置く』というモーションを1つの関数にしたいぜ」

202101__character__28--kifuwarabe-futsu.png
「 せっかく n枚 指定できる作りなのに……」

202101__character__31--ramen-tabero-futsu2.png
「 じゃあ n枚 のところも残すか……」

    /// <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);
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こんな感じで」

📅2023-01-28 sat 19:12

202108__character__12--ohkina-hiyoko-futsu2.png
「 手札を先頭から抜いたら、後ろのカードが浮いたままになってしまうわよ?」

202101__character__31--ramen-tabero-futsu2.png
「 確かに」

202101__character__28--kifuwarabe-futsu.png
「 先頭から抜くのが おかしいのでは? 後ろから抜けだぜ」

202101__character__31--ramen-tabero-futsu2.png
「 もっともだぜ」

    /// <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);
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうかな」

📅2023-01-28 sat 19:51

202301_unity_28-2000--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 画作り を進め中」

202301_unity_28-2003--piles-1.png

202108__character__12--ohkina-hiyoko-futsu2.png
「 👆 スピードをやってて、手札を積み上げるのって 全部で どういうケースがあるの?」

202101__character__31--ramen-tabero-futsu2.png
「 初回で カードを配るときじゃないか?
ゲームが始まったら 手札にカードを積む という動きは無いだろ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 初回は、どこから手札にカードが飛んでくるの?」

202101__character__31--ramen-tabero-futsu2.png
「 どこからでもない。
Unity のシーン上に ゲーム・オブジェクトが適当に散らばっているぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 関数にしたいのよ。
『どこかに置いてあって、それを手札に積む』という定型パターンに乗せたいから、
どこかに置いてあることにしなさいよ」

202101__character__31--ramen-tabero-futsu2.png
「 手札以外には、台札と 場札しかないぜ。
そのどっちかだろ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 じゃあ ゲーム開始時に 散らばっているカードは
台札という扱いにして、ゲーム開始時に 『台札を色分けして、手札に積む』という定型パターンに乗せなさいよ」

    // 台札
    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 };

202101__character__31--ramen-tabero-futsu2.png
「 👆 台札の一番上のカードの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;
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 台札を手札へ移動する動きも 1本化するぜ」

    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);
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 それに引きずられて、開始時の処理も変えるぜ」

📅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);

202101__character__31--ramen-tabero-futsu2.png
「 👆 場札の位置調整を 毎回書くのも煩わしいから 関数の中に入れるかだぜ」

    /// <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);
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 モデルへの編集と、画面への編集は 同じ関数に入れない方がいいんだが、
それは あとで考えるぜ。
関数名も変更」

    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");
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 開始時のコードが かなり短くなっただろ」

📅2023-01-28 sat 21:37

202301_unity_28-2137--picture-making.png

202108__character__12--ohkina-hiyoko-futsu2.png
「 👆 場札の間隔が空きすぎていて、スピードをしてる感じ、しなくない?」

202101__character__31--ramen-tabero-futsu2.png
「 均等割り付け なんだぜ」

202101__character__28--kifuwarabe-futsu.png
「 隙間を詰めてくれだぜ」

202301_unity_28-2147--calculate.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こういう感じか。 まるで プロポーショナル・フォント みたいだな」

202301_unity_28-2208--picture-making.png

202108__character__12--ohkina-hiyoko-futsu2.png
「 👆 カードの境界線が見えないから 1枚の長い紙みたいに見えるわよ」

202101__character__31--ramen-tabero-futsu2.png
「 世界に 境界線というものは 無いので……」

202101__character__28--kifuwarabe-futsu.png
「 カードを少し傾けたらどうだぜ?」

📅2023-01-28 sat 22:09

202301_unity_28-2214--angle.png

    /// <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;
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 2プレイヤー側は いい感じに影が付いたが、1プレイヤー側は 光の当たり方のせいで 思ったようにはなってないぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 扇状に並べたら いい感じになるんじゃない?」

📅2023-01-28 sat 22:17

202301_unity_28-2220--calculate.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 頭の運動不足の頭では n枚のとき角度は何mがいいのか ぱっと出てこないが まあ 手調整してみるか」

202301_unity_28-2227--calculate.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こんな式だったかな?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 Y は空を指してるから、テーブルの奥は Z ね」

📅2023-01-28 sat 22:58

202301_unity_28-2254--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 うーん、 どうすればいいのか。ちょっと 考えようか」

202301_unity_28-2256--idea.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 これで行ってみるかだぜ」

202301_unity_28-2311--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 わたしは恥ずかしい。一発で決めれない」

202108__character__12--ohkina-hiyoko-futsu2.png
「 何発で決まったとか ユーザーには分かんないから 目視確認と 手調整を繰り返せばいいのよ」

202101__character__31--ramen-tabero-futsu2.png
「 半径が 100、スタートの角度が 110°、 間隔の角度は -4° でこれだから、
もっと半径を大きくして カーブを緩くするか」

202301_unity_28-2322--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 半径が 200、スタートの角度が 112°、 間隔の角度は -1.83°、 円の中心のz位置を +10」

202101__character__28--kifuwarabe-futsu.png
「 うまく画面に収めたが、ゲーム中にこんなケースは出てこないのでは?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 盤の周りが スカスカに空いてる分には ユーザーも困らないでしょう」

202301_unity_28-2332--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 2P側も どう見えるか確認だぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 台札は カードが積み重なっていくはずだから、 画面の下側に 気持ち ずらした方がよくない?」

📅2023-01-28 sat 22:58

202301_unity_28-2339--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 几帳面に真上に積むから 2Pのカードと被って見えることは無さそうだぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 几帳面に積まれるのも スピードな感じは しないわねえ」

202301_unity_28-2359--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 ランダムにずれを入れると どこに行くか わからんけど」

202108__character__12--ohkina-hiyoko-futsu2.png
「 もっと 上下の空いてる方向に 伸びなさいよ」

202301_unity_29-0007--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうかだぜ?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 スピードな感じは しないわねえ」

202101__character__28--kifuwarabe-futsu.png
「 台札の所定の位置から離れていくのが おかしいのと、
カードが几帳面に 正方形の角度が ぶれてないのが おかしいんだぜ」

202101__character__31--ramen-tabero-futsu2.png
「 なるほど」

202301_unity_29-0021--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうかだぜ?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 少しは 揃えて置こうと してほしいわねえ」

202101__character__28--kifuwarabe-futsu.png
「 1プレイヤー、2プレイヤーが 右利きか、左利きかでも 変わってくるんじゃないかだぜ?」

202101__character__31--ramen-tabero-futsu2.png
「 そんなん 設定するのも 嬉しさがあるのか分からないので 右利き ということにしとこうぜ?」

202301_unity_29-0042--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 少しは揃えてみたのと、1プレイヤーと 2プレイヤーで 大きく捻る回転方向を ずらしたぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 良くは なってきたわね」

    /// <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

202301_unity_29-0049--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 カードのセンタリングが まだ作ってないので 作るぜ」

202301_unity_29-0054--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 なんか 手札が めっちゃ遠くに見えるな」

202101__character__28--kifuwarabe-futsu.png
「 そりゃ、25枚の場札に合わせた位置だしな」

ソースコード:

    /// <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;
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 このコードは まあ こんなもんだろ」

202301_unity_29-0102--picture-making.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 目視確認と 感覚で 調整」

202108__character__12--ohkina-hiyoko-futsu2.png
「 画作り は、こんなもんでいいでしょ」

📅2023-01-29 sat 01:04

202108__character__12--ohkina-hiyoko-futsu2.png
「 次は 入力 を作っていきましょう」

202108__character__12--ohkina-hiyoko-futsu2.png
「 カーソル・キーの 左、右で ピックアップしている場札を 隣の札に変えるようにしましょう」

202101__character__31--ramen-tabero-futsu2.png
「 すると、今どの場札に フォーカスが当たっているかを 変数として持ちたいよな」

202101__character__31--ramen-tabero-futsu2.png
「 でも 今日はここまでだぜ」

202101__character__28--kifuwarabe-futsu.png
「 おつ」

📅2023-01-29 sat 01:23

📅2023-01-29 sat 15:05

202101__character__31--ramen-tabero-futsu2.png
「 入力を受け取る部分を まず書くか」

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プレイヤー視点で言うと)右隣の場札を選択する
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 スピードの操作って 他にある?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 だいたい こんなもんでしょう。
あとで 気づいたら そのとき 追加しましょう」

📅2023-01-29 sat 15:24

Assets.Scripts.GameManager.cs :

    /// <summary>
    /// プレイヤーが選択しているカードは、先頭から何枚目
    /// </summary>
    int[] playsersFocusedCardIndex ={ 0, 0 };

202101__character__31--ramen-tabero-futsu2.png
「 👆 何枚目のカードを選択しているか、覚えさせることにするぜ」

    /// <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);
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうかなあ?」

📅2023-01-29 sat 15:42

202101__character__28--kifuwarabe-futsu.png
「 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:
                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;
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうかなあ?」

📅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);

202101__character__31--ramen-tabero-futsu2.png
「 👆 これは」

新しいコード:

        // 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);

202101__character__31--ramen-tabero-futsu2.png
「 👆 こう書き直せるな」

202108__character__12--ohkina-hiyoko-futsu2.png
「 『フォーカスされているカードはない』という状態を 有りにすれば、
『1枚目のカードにフォーカスを当てる』のも、
『1プレイヤーの右隣のカードへフォーカスを移します』で代用できるんじゃないの?」

    /// <summary>
    /// プレイヤーが選択している場札は、先頭から何枚目
    /// 
    /// - 選択中の場札が無いなら、-1
    /// </summary>
    int[] playsersFocusedCardIndex = { -1, -1 };

202101__character__31--ramen-tabero-futsu2.png
「 👆 こう書き直して……」

    /// <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;
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうかだぜ」

        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);

202101__character__31--ramen-tabero-futsu2.png
「 👆 すると こう書けるわけかだぜ。 便利になるな」

📅2023-01-29 sat 16:03

202101__character__28--kifuwarabe-futsu.png
「 持ってる場札を、台札の上に置いたら、
場札は どれをピックアップしている状態に戻るんだぜ?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 どれもピックアップしてないんじゃないの?」

202101__character__28--kifuwarabe-futsu.png
「 じゃあ また端っこから ピックアップし直しかだぜ?」

202101__character__31--ramen-tabero-futsu2.png
「 それも不便だぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 じゃあ 抜いたカードの右隣を ピックアップするようにしたらいいんじゃないの?」

202101__character__28--kifuwarabe-futsu.png
「 右端のカードを抜いたんだったら どうする?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 一番右端のカードを ピックアップしたらいいのよ。
何もピックアップしないというのも 自然だけど」

📅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");
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 既存のコードでは、任意の場所のカードを引き抜けたが、
これを ピックアップしているカードを引き抜く ように固定したいぜ」

    private void MoveCardToCenterStackFromHand(int player, int handIndex, int place)
    {
        var goCard = goPlayersHandCards[player].ElementAt(handIndex); // カードを1枚抜いて
        goPlayersHandCards[player].RemoveAt(handIndex);

202101__character__31--ramen-tabero-futsu2.png
「 👆 つまり handIndex 変数を廃止して、
playsersFocusedCardIndex[player] を参照するように 1本化したいぜ」

202101__character__28--kifuwarabe-futsu.png
「 しろだぜ」

    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);
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こう書きかえて」

    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");
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうだぜ」

📅2023-01-29 sat 16:44

202101__character__31--ramen-tabero-futsu2.png
「 場札を並べ直すと、場札を全部 盤の上に置いてしまって ピックアップを忘れているぜ」

    /// <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);
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 再度、持ち上げるのを関数化して……」

    /// <summary>
    /// 場札を並べる
    /// </summary>
    void ArrangeHandCards(int player)
    { // ... 略

        // 場札を並べなおすと、持ち上げていたカードを下ろしてしまうので、再度、持ち上げる
        ResumeCardPickup(player);
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 場札の並べなおしメソッドに組み込んでしまおう。
他の箇所の既存の 持ち上げ直しコードは消すぜ」

📅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);
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 半分埋まったな」

202108__character__12--ohkina-hiyoko-futsu2.png
「 2プレイヤーは 頭がひっくり返って 不利じゃない?」

202101__character__31--ramen-tabero-futsu2.png
「 上級者用のハンデに使えだぜ」

📅2023-01-29 sat 17:18

202101__character__31--ramen-tabero-futsu2.png
「 動かしてみると 配列の範囲を超えるエラーと よく出会うので リミットチェックを入れていくぜ」

    /// <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);
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こう」

📅2023-01-29 sat 17:40

202301_unity_29-1744--game.png

202108__character__12--ohkina-hiyoko-futsu2.png
「 右端の場札を 台札の上に置いたら、
何も場札を選択していない状態になるんだけど、
この瞬間、何ボタンを押したらいいのか 分かんなくなっちゃうのよね」

202101__character__31--ramen-tabero-futsu2.png
「 じゃあ、場札がある限り、必ず なんらかの場札が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);
        }
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 左右への移動は 右端が左端とつながっているようにループさせて」

    /// <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; // 更新:何枚目の場札をピックアップしているか

// ... 略

202101__character__31--ramen-tabero-futsu2.png
「 👆 台札の移動は 場札のどれかを必ず選択しておくようにするぜ」

📅2023-01-29 sat 18:03

📅2023-01-31 mon 04:32

202101__character__31--ramen-tabero-futsu2.png
「 腰が痛くて 目が覚めたので 朝から練習しよ。
えーと どこまで やったっけ?」

202301_unity_31-0436--morning.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 フーン。
何やってたかな。手札を、場札へ移動する動きも付けたいな。
デバッグ用に スペース・キーに割り当てるか」

    void Update()
    { // ...

        // デバッグ用
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 両プレイヤーは手札から1枚抜いて、場札として置く
            for (var player=0; player<2; player++)
            {
                MoveCardsToHandFromPile(player, 1);
            }
        }    
    }

202101__character__31--ramen-tabero-futsu2.png
「 👆 こんなんでいいかな」

202301_unity_31-0443--test.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 よし、手札を場札へ 移動できるぜ」

202101__character__31--ramen-tabero-futsu2.png
「 操作方法を 画面に書き込むかな?」

202301_unity_31-0501--png.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 どうやって 白色を 透過させるんだぜ?」

📖 透過PNGの設定方法 Unity

202301_unity_31-0522--png-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 ゲーム・オブジェクトじゃなくて、画像素材側に設定するのかだぜ?」

202301_unity_31-0528--texture-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 ウィンドウの縦幅を伸ばすと Apply ボタンが出てきた。気づかね~」

202301_unity_31-0531--single.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 べつに 白色が透過される様子もないぜ」

📖 unityで画像背景を透過させる方法が分かりません。
📖 Unityでテクスチャを透明にする

202301_unity_31-0542--gimp-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 調べても分からんので昔ながらの方法でやる。GIMP を使って、
PNG画像が RGB 形式で、データが 不透明度を表す A チャンネルを持ってないようなので、
アルファ・チャンネルを追加し……」

202301_unity_31-0546--gimp-b-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 マジックワンドを使って 白い所を選んで、 [Del] キーで消すぜ」

202301_unity_31-0549--transparent.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 画像を差し替えると 透過した」

202301_unity_31-0610--sprite-diffuse-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 この ShaderSprites/Diffuse でいいのか?
自分が何やってんのか 分からなくて つらい」

📅2023-01-31 sat 06:12

202301_unity_31-0637--key-explain.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 操作説明は これぐらい うるさく書いておけば 確かだろ。
邪魔くさいが……」

📅2023-01-31 sat 06:38

202101__character__31--ramen-tabero-futsu2.png
「 👆 やっぱ RGB値を 加算したいな……。
シェーダーの書き方を調べるか」

📖 【Unity UI】uGUIで色を加算合成する方法

202101__character__31--ramen-tabero-futsu2.png
「 👆 日本人の薄っぺらい記事なんか読んでも さっぱり分からんな 公式読むか」

📖 カスタムシェーダーの基礎
📖 シェーダーの作成

202101__character__31--ramen-tabero-futsu2.png
「 👆 HLSL という書き方を覚えればいいらしいぜ」

202101__character__31--ramen-tabero-futsu2.png
「 数十分で終わるレベルではないので パス」

📅2023-01-31 sat 06:58

202101__character__31--ramen-tabero-futsu2.png
「 次は、ゲームのルールを組み込んでいくか」

202101__character__31--ramen-tabero-futsu2.png
「 しかし、今のプログラミングの書き方では ルール・ベースで書きにくい……」

202301_unity_31-0715--game-view-model-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 いったん 画面関係の変数は GameViewModel クラスを新しく作って そっちへ移し……」

202301_unity_31-0717--game-manager-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 GameManager.cs の方では GameViewModel インスタンスを使うように書き直し……」

202301_unity_31-0724--game-view-model-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 変数名の頭に go と付けたやつは GameObject なんで、
画面に関係するものは全部 GameViewModel クラスへ 追いやるぜ」

202301_unity_31-0728--wrapping.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 配列の長さを取りたいときは、 GetCenterStackCardsLength() メソッドを使うようにする。
これを ラッピング・メソッド(Wrapping method) という」

202301_unity_31-0731--game-manager-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こうやって、 GameManager の方は、 ゲーム・オブジェクトをいじらなくても、
player: 0 とか、 numberOfCards: 5 とか、命令だけ書けばいいような 見た目に変えていくぜ」

📅2023-01-31 sat 07:33

202101__character__31--ramen-tabero-futsu2.png
「 全部 ラッピングするの 時間かかるから 今朝は ここまで」

📅2023-01-31 sat 08:04

📅2023-01-31 mon 23:34

202101__character__31--ramen-tabero-futsu2.png
「 眠りが覚めた。ちょっと練習しよ」

📅2023-02-01 sat 23:34 start

202101__character__31--ramen-tabero-futsu2.png
「 全体的に 書きかえた。また寝よ」

📅2023-02-01 sat 03:54 end

📅2023-02-01 mon 21:04

202302_unity_01-2123--explorer.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 大改造した」

202101__character__28--kifuwarabe-futsu.png
「 いっぱい増えてるぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 会社でやってはいけないことの1つが 大改造よ。
コード・レビュー受け付けられないから 没になるのよ」

202302_unity_01-2127--timeline.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 Unity に ゲームの基本機能が 足りな過ぎるので タイムライン機能を自作した」

202101__character__28--kifuwarabe-futsu.png
「 Unity にも あるかも知らんのに」

202302_unity_01-2129--command.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 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);
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 『コマンドを実行すると、 ゲームの内部状態 と、 画面表示の状態 が変わる』、ということを書いている」

📄 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);
            }
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 コマンドの中身は こんな感じだぜ」

202101__character__28--kifuwarabe-futsu.png
「 さっぱり分からん」

📄 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;
            }
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 説明するの めんどくさいんで 感じろだぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 わらう」

📄 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;
        }
    }
}

202101__character__28--kifuwarabe-futsu.png
「 👆 説明がないのなら コードも貼らなくていいのでは?」

📄 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);
            }
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 まあ、 だから ゲームの状態と、 画面の表示を 変更するのが コマンドだぜ」

202302_unity_01-2144--timeline-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 そして コマンドに 時間を付けて、 時限式で 実行すりゃいいんだぜ。
ソースを見てみよう」

📄 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; }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 タイムラインの上に置いてあるコマンドだぜ。
『音符』みたいなもんだぜ。 感じろ」

📄 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;
                    }
                }
            }
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 タイムラインは、『音符』のようなものが記憶されていて、時間が来たら実行される。
『楽譜』みたいなもんだぜ。 感じろ」

202302_unity_01-2151--game-model-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 ゲームの状態を記憶しているのは、 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];
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 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);
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 GameModelBuffer は、ゲームを時間で切った断面図みたいなもんだぜ」

202101__character__28--kifuwarabe-futsu.png
「 じゃあ スナップショットか?」

📄 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,
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 トランプのカードの Id を、 enum型で作っておくぜ」

📅2023-02-01 sat 22:04

202302_unity_01-2210--view-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 ビューは 画面表示関連だぜ」

📄 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);
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 ゲーム・オブジェクトを、 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();
            }
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 ゲーム・ビュー・モデルは 画面の表示が どんな感じになってるか記憶したり、編集したりしているな」

202302_unity_01-2216--game-manager-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 一番上の階層のスクリプトは、整理できてないものが残っている」

202101__character__28--kifuwarabe-futsu.png
「 目立つんだから、重要なものを置けだぜ」

📄 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°回転
    //}
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 PlayingCard.cs は、神経衰弱ゲームのとき使っていたが、スピードでは使っていないぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 じゃあ 消しなさいよ!」

📄 Assets/Scripts/LazyArgs.cs file:

namespace Assets.Scripts
{
    /// <summary>
    /// コーディングのテクニックのための仕込み
    /// </summary>
    internal class LazyArgs
    {
        public delegate void Action();
        public delegate void SetValue<T>(T value);
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 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;
        }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 キー入力しても、コマンドは ただちに実行せず、
いったん タイムラインに登録するというのが、
ビューと モデルを分離した工夫だぜ」

202101__character__28--kifuwarabe-futsu.png
「 最初から そうしろだぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 Unity の開発経験が 短いからね」

📅2023-02-01 sat 22:24

202108__character__12--ohkina-hiyoko-futsu2.png
「 Lerp (リープ) を使うと、モーションを補間できるんじゃないの?」

202101__character__31--ramen-tabero-futsu2.png
「 やってみるかだぜ」

📄 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);
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 例えば、場札を持ち上げるのは、 positionrotation を上書きしていたが、
これをやめて、
持ち上げる前の positionrotation
持ち上げた後の positionrotation を持てばいいわけだぜ」

202101__character__28--kifuwarabe-futsu.png
「 それを覚えておいて、 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;
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 この beginPositionendPositionbeginRotationendRotation
呼出し元へ さかのぼって持っていけばいいのか、大変だな」

📺 開発中画面

202101__character__31--ramen-tabero-futsu2.png
「 👆 今日は ここまでだぜ」

📅 2023-02-01 sat 23:58 end

📅2023-02-02 thu 18:44

📺 作業用BGM

202302_unity_02-1844--movement-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 とりあえず、 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; }
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 Lerp に使うデータを 持っておくクラスだな」

書き直す前のソース:

        /// <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);
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 動作が変わるんで リファクタリング ではなくて 仕様変更だが、
どんどん Lerp するコードを 関数の外側に出すための 仕込みをしていこう」

202302_unity_02-1927--unwrapped-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 SetPosRot という関数そのものが よくないので、この関数は削除して
呼出し側に ベタ書き するように変えていこう」

202108__character__12--ohkina-hiyoko-futsu2.png
「 最大25枚の場札を 円弧上に 揃えて並べていく処理よ それ」

202101__character__31--ramen-tabero-futsu2.png
「 じゃあ、タイムラインには コマンドだけではなくて、
Movement も置けた方がいいのか」

202101__character__28--kifuwarabe-futsu.png
「 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();
    }
}

202101__character__31--ramen-tabero-futsu2.png
「 👆 DoIt を、 OnEnter に書き直すだけでも 一貫性が出てきそうだぜ」

202101__character__28--kifuwarabe-futsu.png
「 コマンドに 開始時間と 持続時間 を持たせてしまえば どうだぜ?
名前を TimeSpan にでも変えて、 コマンドはその特殊なケースにしろだぜ」

202302_unity_02-2041--time-span-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 TimedItem と、 Command と、 Movement は、 TimeSpan という枠組みで1本化したぜ」

📅 2023-02-01 sat 20:46 end

202101__character__31--ramen-tabero-futsu2.png
「 どんどん Lerp を使える形に変えていこう」

📅 2023-02-01 sat 21:02

202302_unity_02-2100--put-down-card-of-hand-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 例えば PutDownCardOfHand メソッドは、単に Movement インスタンスを作るだけのメソッドだと分かった」

202108__character__12--ohkina-hiyoko-futsu2.png
「 頭の中が整理されてきたのね」

202101__character__28--kifuwarabe-futsu.png
「 頭を整理してから プログラミングしてくれだぜ」

        /// <summary>
        /// 場札を並べる
        /// 
        /// - 左端は角度で言うと 112.0f
        /// </summary>
        internal void ArrangeHandCards(GameModel gameModel, int player, LazyArgs.SetValue<List<ISpan>> setSpans)
        { // ...

202101__character__31--ramen-tabero-futsu2.png
「 👆 例えば ArrangeHandCards メソッドは、単に Movement インスタンスを複数、作るだけのメソッドだと分かった」

202302_unity_02-2123--bug.png

📅 2023-02-01 sat 21:24

202101__character__31--ramen-tabero-futsu2.png
「 👆 あっ、バグった!」

202101__character__28--kifuwarabe-futsu.png
「 表現しづらいバグを起こすなあ。
持ち上げるカードを 間違えているんだぜ なぜか」

202302_unity_02-2132--fixme-code-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 タイム・スパンの インスタンスの中で 即実行 するコードがあると タイミングが狂ってる感じがするぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 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);
                }
            }
        }

202101__character__31--ramen-tabero-futsu2.png
「 👆 なるほど」

202302_unity_02-2139--sub-spans.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 ダメだぜ。持ち上げるカードを間違えているぜ」

202101__character__28--kifuwarabe-futsu.png
「 コマンドのタイム・スパンの寿命と、
場札の位置調整をしたいタイム・スパンの寿命は 別物だからでは?」

202108__character__12--ohkina-hiyoko-futsu2.png
「 じゃあ タイム・スパンを新しく スポーン(Spawn;生成)しなくちゃ いけないのよ」

202302_unity_02-2143--timeline-model-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 タイムライン・モデルは ゲーム・モデルの外側にあるし、 ゲーム・ビュー・モデルの外側でもあるぜ。
タイムライン・モデルは モデル なのだろうか? ビュー なのだろうか?」

202101__character__28--kifuwarabe-futsu.png
「 画面に表示されてないから、モデルだろ」

202101__character__31--ramen-tabero-futsu2.png
「 モデルのくせに、ゲーム・オブジェクトを持ってるのは 良くない」

202108__character__12--ohkina-hiyoko-futsu2.png
「 タイムライン・モデルの モデルとビューの分離 を先に行っておくべきだったんじゃない?」

202302_unity_02-2148--discard.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 今なら Discard(ディスカード;更新の破棄)しても ダメージは30分レベルで軽微だから ロールバック(Rollback;巻き戻し)するかだぜ」

📅 2023-02-01 sat 21:49

202101__character__31--ramen-tabero-futsu2.png
「 巻き戻した」

202302_unity_02-2154--model-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 モデルが ビューを持ってると 良くないんだぜ」

202302_unity_02-2159--lerp-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 じゃあ Lerp メソッドのような、ビュー に属するものを タイムライン・モデルが 持っていては
いけなくないかだぜ?」

202101__character__28--kifuwarabe-futsu.png
「 そりゃそうだぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 TimelineView のようなものが 要るのかしら?」

202302_unity_02-2216--model-view-1.png

📅 2023-02-02 sat 22:20

202101__character__31--ramen-tabero-futsu2.png
「 👆 OnEnter メソッドは モデルを扱い、
Lerp メソッドは ビューを扱うというように ぱっきり 分かれているので、
TimeSpan も モデルとビューに分けられないかだぜ?」

202101__character__31--ramen-tabero-futsu2.png
「 あっ、トランプの絵柄を取得できるメソッドが欲しいぜ」

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

202101__character__31--ramen-tabero-futsu2.png
「 👆 作った」

202302_unity_03-0200--bug-point-1.png

📅 2023-02-03 sat 02:00

202101__character__31--ramen-tabero-futsu2.png
「 👆 タイム・ライン登録時は まだ 座標が動いてないから、
タイム・ライン登録中も 座標を動かしてやらないと いけないぜ。
この不具合は また今度直そう」

📺 開発中画面

202101__character__31--ramen-tabero-futsu2.png
「 今日は ここまで」

📅2023-02-03 fri 18:48

202101__character__31--ramen-tabero-futsu2.png
「 しばらく 作り直し」

202302_unity_04-0206--addition-1.png

📅 2023-02-04 sat 02:07

202101__character__31--ramen-tabero-futsu2.png
「 👆 7時間ぐらいバグ探して 1つ 直した。
操作を2連続で行うと カードが変なところに飛んでいくので、
2つの操作を結合して、1回の操作にする」

📺 開発中画面

202101__character__31--ramen-tabero-futsu2.png
「 今日はここまで」

📅2023-02-04 sat 12:02

202101__character__31--ramen-tabero-futsu2.png
「 作ってみると 分からないところが いっぱい出てくる。散々だぜ」

202302_unity_04-1351--static-game-view-1.png

📅 2023-02-04 sat 13:53

202101__character__31--ramen-tabero-futsu2.png
「 👆 GameViewModel を廃止し、静的クラスにしたぜ」

202302_unity_04-1941--miss-name-1.png

📅 2023-02-04 sat 19:43

202101__character__31--ramen-tabero-futsu2.png
「 👆 怪しい場所を発見したぜ」

202101__character__31--ramen-tabero-futsu2.png
「 修正したが、バグが直る様子なし」

202302_unity_04-2119--log-1.png

📅 2023-02-04 sat 21:20

202101__character__31--ramen-tabero-futsu2.png
「 👆 5.0 だけ持ち上げたいのに、すでに 25.5 も持ち上がってたら、 6倍ぐらい 飛び上がるよな」

202101__character__28--kifuwarabe-futsu.png
「 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 だぜ」

202108__character__12--ohkina-hiyoko-futsu2.png
「 加速していく 5.0 を累計してんの?」

202101__character__31--ramen-tabero-futsu2.png
「 なんで勝手に 積分してんだぜ? 調べるか……」

202302_unity_04-2144--sum-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 説明するのは難しいが 理解した。
開始地点から 終了地点まで 刻んで動け、という命令をしてるときに
開始地点が 刻々と 進んでいるんだぜ」

202101__character__28--kifuwarabe-futsu.png
「 開始地点は 動くなだぜ」

202302_unity_04-2201--bug-fix-1.png

202101__character__31--ramen-tabero-futsu2.png
「 👆 こういう書き方で 修正できたが、
こんな書き方が役に立つ場面 初めて見た。
不思議な気分だぜ」

📺 開発中画面

📅 2023-02-04 sat 23:55

202101__character__31--ramen-tabero-futsu2.png
「 👆 思ったのと 違う動きをしているが、
不具合のリクツが分かってきたのは前進だぜ」

202101__character__31--ramen-tabero-futsu2.png
「 今日は ここまでだぜ」

📅2023-02-05 sun 12:27

202101__character__31--ramen-tabero-futsu2.png
「 バグ探ししてるだけで 進展なし」

202302_unity_05-1752--modified-1.png

📅 2023-02-05 sat 17:53

202101__character__31--ramen-tabero-futsu2.png
「 👆 1か所 変更して 様子を見るぜ」

📺 開発中画面

📅 2023-02-05 sat 18:02

202101__character__31--ramen-tabero-futsu2.png
「 👆 カードが飛び上がるのは なくなったぜ」

202101__character__28--kifuwarabe-futsu.png
「 連打を禁止したら どうだぜ?
Lerp が重なってるケースがあるのでは?」

202302_unity_05-1808--input-manager-1.png

📅 2023-02-05 sat 18:09

202101__character__31--ramen-tabero-futsu2.png
「 👆 入力系は こんがらがると 大変だろうから ゲーム・マネージャーと分離するぜ」

202101__character__31--ramen-tabero-futsu2.png
「 疲れた ラーメン食べに行く いったん休憩!」

202101__character__28--kifuwarabe-futsu.png
「 おつ」

📅 2023-02-05 sat 18:10 end

📅2023-02-06 sun 02:06

202101__character__31--ramen-tabero-futsu2.png
「 3時間寝たと思ったら目が覚めた。練習しよ」

202302_unity_06-0326--bug-fix-1.png

📅 2023-02-06 mon 03:28

202101__character__31--ramen-tabero-futsu2.png
「 👆 やることの順番を 少しでも 前後間違えると 違った動きをする。
直した」

📺 開発中画面

202101__character__28--kifuwarabe-futsu.png
「 👆 やったな!」

📺 開発中画面

📅 2023-02-06 mon 04:13

202101__character__31--ramen-tabero-futsu2.png
「 👆 今朝は ここまで」

202101__character__28--kifuwarabe-futsu.png
「 おつ」

// 書きかけ

ツイッターでシェア
みんなに共有、忘れないようにメモ

むずでょ@きふわらべ第29回世界コンピューター将棋選手権一次予選36位

光速のアカウント凍結されちゃったんで……。ゲームプログラムを独習中なんだぜ☆電王戦IIに出た棋士もコンピューターもみんな好きだぜ☆▲(パソコン将棋)WCSC29一次予選36位、SDT5予選42位▲(パソコン囲碁)AI竜星戦予選16位

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント