ぐーるらいふ

底辺。

【Unity】第17回「あける」コネクト【unity1week】

お久しぶりです。約三か月ぶりですね。
また書いてます。なんでしょうね、こう辞めれないのは。

f:id:ghoul_life:20201228003111g:plain

unityroom.com

一番大変だったこと

いきなりこっから話します。そして正直に言います。

クリスマス絵

です。

絵を描いているとこういったクリスマス、バレンタイン、年末年始といったイベント時は
絵を描いてUPするのが定番となっています。

これをやらないとグループに残れないレベルで深刻です。
なので12/21~12/24までの間、最優先はクリスマス絵でした。
仕事をしていた方も多いと思く自分も例に漏れませんでしたが、
帰宅後もUnityを開くことは無く、クリスマス絵に100%の力を注いでいました。

こちらは無事に12/24に提出完了しました。

はじめに

どうやらunity1week始まってるらしいな、とnaichiさんのTwitterを見て知りました。
この時点で12/25。

ヤバイですね!

というわけで

作業を始めました。まずはUnityのアップデートをしつつ、考えを巡らします。
「あける」ってどうしようかなぁと思っていたら。

f:id:ghoul_life:20201227235132j:plain

そうこれですよね。そうです。ペルソナです。
あー、これやりたいなぁと思って
「姫にキスしたら目をあける」
「いや、ファンタジー描けないしSF娘にするか」
と後ろ向きな理由で

アンドロイドみたいなロボットが起動する(目をあける)

というゲームにしようと
ざっくり考えました。

そこから

そっからは早かったです。
とりあえずパーツをはめて電源からハートを繋ぐゲームにしようと考え、
素材を準備していきます。

f:id:ghoul_life:20201228000433p:plain

今回のドット描きはEDGEです。
高機能ドット絵エディタ EDGE | TAKABO SOFT

普段はCLIP STUDIOしか使わないんですが、
ちょっと別のツールにも慣れておこうかなと使ってみました。
ウィンドウとかパレット周りにクセがあり、ワークスペース制になってくれると
いいなぁと思いながら使ってました。
まぁ、慣れると結構いけます。
ドットアニメーション周りも全部EDGEです。
(選択アニメーションや背景の線とかも)

コーディング

絵を描きながら頭でイメージコーディングしておきます。

「あ、このパーツはこう使おうとしてるからこんな感じのコードが必要になるな」

という考え方です。
それに合わせて書いていきます。

まぁほとんどピース周りのコーディングです。

public partial class Piece : MonoBehaviour
{
    protected int _pieceId = 0; // 一意に識別するID
    protected bool _isUse = false; // ユーザーに使用中か
    protected bool _isConnect = false; // 接続できているか?
    protected bool _isCalcurate = false; // ルート計算済みならtrue
    protected bool _isUsePool = false; // プールから使われているか?

    protected PieceData _pieceData = null;
    protected RectTransform _rectTransform = null;
    protected System.Action<Piece> _onClickEvent = null;

    private Vector2 _pieceFirstPosition = Vector2.zero;
    private Vector2 _piecePosition = Vector2.zero;

    private bool _isAnimation = false;
    private RectTransform _iconParent = null;

    // Start is called before the first frame update
    public virtual void InitializeEditor(PieceType pieceType , System.Action<Piece> onClickEvent, RectTransform rectTransform)
    {
        _pieceData = new PieceData(pieceType);
        _onClickEvent = onClickEvent;
        _pieceLock.SetLock(false);
        _pieceLock.SetVisible(false);
        _isCalcurate = false;
        _iconParent = rectTransform;
        UpdateIcon();
        SetConnect(false);
        UnUse();
        SetButtonActive(true);
    }
    //~~~ 略 ~~~

    //--------------------------------------------
    // use pool

    public void UsePool()
    {
        if (!_isUsePool)
        {
            _isUsePool = true;
            _canvasGroup.alpha = 1;
            this.gameObject.SetActive(true);
        }
    }

    public void UnUsePool()
    {
        _isUsePool = false;
        BackOverLayIcon();
        PieceCreater.Instance.UnUsePoolPiece(this.gameObject);
        this.gameObject.SetActive(false);
    }

    public bool IsUsePool
    {
        get
        {
            return _isUsePool;
        }
    }

 //~~~~~
}

ここで「おっ」と思った方もいると思いますが、
今回のゲームで使うオブジェクトは全てPoolシステムに乗っかっています。
これもイメージコーディングで

このパーツは大量に必要になるからpoolが必要になるな

と想定して使う前提のコーディングをします。

// ※かなり色々はしょっています。
// 必要な所だけ載せてます
public class PieceCreater : SingletonMonoBehavior<PieceCreater>
{
    public const int COUNT = 100;

    [SerializeField] private Piece _piecePrefab = null;
    [SerializeField] private RectTransform _rectTransform = null;

    private List<Piece> _piecePool = null;
    private bool _isInitialize = false;

    public void Initialize()
    {
        if(!_isInitialize)
        {
            _isInitialize = true;

            _piecePool = new List<Piece>();

            for (var i = 0; i < COUNT; i++)
            {
                var p = Instantiate(_piecePrefab, _rectTransform);
                p.gameObject.name = "Piece_Pool_" + (i + 1);
                p.UnUsePool();
                _piecePool.Add(p);
            }

        }
        else
        {
            if(_piecePool != null)
            {
                foreach (var p in _piecePool)
                {
                    p.UnUsePool();
                }
            }
        }
    }

    public void UnUsePoolPiece(GameObject p)
    {
        if(p != null && _rectTransform != null)
        {
            p.transform.SetParent(_rectTransform);
        }
    }

    public Piece GetPieceByPool()
    {
        if(_piecePool != null)
        {
            var piece = _piecePool.FirstOrDefault(p => !p.IsUsePool);
            if(piece != null)
            {
                piece.RectTransform.pivot = new Vector2(0.5f, 0.5f);
                piece.RectTransform.position = Vector3.zero;
                piece.RectTransform.localPosition = Vector3.zero;
                piece.RectTransform.anchorMax = new Vector2(0.5f, 0.5f);
                piece.RectTransform.anchorMin = new Vector2(0.5f, 0.5f);
                piece.UsePool();

                return piece;
            }
            else
            {
                UnityEngine.Debug.LogError("All Using Piece !!! Please Size Up Pool.");
            }
        }
        else
        {
            UnityEngine.Debug.LogError("Don't create Piece Pool. Please call PieceCreater.getInstance.Initialize()");
        }
        return null;
    }
    // ~~~略~~~
}

こんな感じで全部poolから使うようにして動作の高速化を図ります。

ステージエディタ

面を作るのがとにかく面倒です。
こういうゲームはステージエディタを作ります。
これを作る事によって機能のデバッグにもなります。

f:id:ghoul_life:20201228005843p:plain

容量オーバーでここには上げられなかったのでTwitter経由に
なりますが、動画はこちら。

選択したピースを好きに配置し、レーザーの反射も自由に計算。
そしてjson出力、ロードまで完備してステージクリエイト機能と
各ピースの個別機能のテストも行います。

難しくしすぎないように気を付けてステージを作りました。

レーザーの反射について

ここが今回の開発の急所です。
反射プログラムです。

反射は無理やりMesh変形させて作っていくのもアリなんですが、
単純に本数を増やす事にしました。

つまり一回反射したら二本に、
二回反射したら三本になる
という疑似的な反射にすることにしました。

// Poolシステムに乗っけて、反射の速度に
// ついていけるように高速化は必須です。
public class Laser : MonoBehaviour
{
    private bool _isUsePool = false;
    private const float LASER_SIZE = 50.0f;
    private const float OUT_OF_LASER = 1000.0f;

    public enum LaserDirection
    {
        None = 0, // 通過
        LEFT,
        DOWN,
        RIGHT,
        UP,
        GUARD // 止める
    }

    [SerializeField] private Image _laserImage = null;
    [SerializeField] private RectTransform _rectTransform = null;

    private Piece _startPiece = null;
    private Piece _endPiece = null;
    private Vector2 _startPosition = Vector2.zero;
    private Vector2 _endPosition = Vector2.zero;
    private LaserDirection _laserDirection = LaserDirection.LEFT;

    public void SetUp(Piece piece , LaserDirection laserDirection)
    {
        _startPiece = piece;
        _endPiece = null;
        _startPosition = _startPiece.RectTransform.anchoredPosition;
        _laserDirection = laserDirection;
    }

    public void SetLaserEnd(Piece endPiece)
    {
        _endPiece = endPiece;

        SetSize(_startPiece, _endPiece, _laserDirection);
    }

    /// <summary>
    /// レーザーを画面外で飛ばす
    /// </summary>
    public void SetLaserOutOfBounds()
    {
        var anchorStart = _startPiece.RectTransform.anchoredPosition;
        var anchorEnd = anchorStart;

        switch(_laserDirection)
        {
            case LaserDirection.LEFT:
                anchorEnd.x -= OUT_OF_LASER;
                break;
            case LaserDirection.RIGHT:
                anchorEnd.x += OUT_OF_LASER;
                break;
            case LaserDirection.UP:
                anchorEnd.y += OUT_OF_LASER;
                break;
            case LaserDirection.DOWN:
                anchorEnd.y -= OUT_OF_LASER;
                break;
        }

        SetSize(anchorStart, anchorEnd, _laserDirection);
    }

    //--------------------------------------------
    // set size
    //--------------------------------------------
    private void SetSize(Vector2 anchorStart , Vector2 anchorEnd , LaserDirection direction)
    {
        _rectTransform.localEulerAngles = new Vector3(0, 0, 90 * ((int)direction - 1));

        var sizeDelta = Vector2.zero;
        sizeDelta.y = LASER_SIZE;

        switch (direction)
        {
            case LaserDirection.LEFT:

                sizeDelta.x    = Mathf.Abs(anchorEnd.x - anchorStart.x);
                anchorStart.x -= sizeDelta.x;
                break;
            case LaserDirection.RIGHT:
                sizeDelta.x    = Mathf.Abs(anchorEnd.x - anchorStart.x);
                anchorStart.x += sizeDelta.x;
                break;
            case LaserDirection.UP:
                sizeDelta.x    = Mathf.Abs(anchorEnd.y - anchorStart.y);
                anchorStart.y += sizeDelta.x;
                break;
            case LaserDirection.DOWN:
                sizeDelta.x    = Mathf.Abs(anchorEnd.y - anchorStart.y);
                anchorStart.y -= sizeDelta.x;
                break;
        }
        _rectTransform.sizeDelta = sizeDelta;
        _rectTransform.anchoredPosition = anchorStart;
    }
}


レーザーは全てこのコントローラーが処理を行います。

public class LaserController : MonoBehaviour
{
    private RectTransform _targetRectTransform = null;

    private System.Action _onGameClearEvent = null;
    private List<Laser> _laserList = null;
    public void SetUp(RectTransform rectTransform , System.Action onGameClearEvent)
    {
        _targetRectTransform = rectTransform;
        _onGameClearEvent = onGameClearEvent;
    }

    public void Dispose()
    {
        if(_laserList != null)
        {
            foreach(var l in _laserList)
            {
                l.UnUsePool();
            }
            _laserList.Clear();
            _laserList = null;
        }
    }

    private Laser CreateLaser(Piece piece , Laser.LaserDirection laserDirection)
    {
        if(_laserList == null)
        {
            _laserList = new List<Laser>();
        }
        var laser = PieceCreater.Instance.GetLaserByPool();
        laser.RectTransform.SetParent(_targetRectTransform);
        laser.SetUp(piece, laserDirection); // 開始位置と方向
        _laserList.Add(laser);

        return laser;
    }

    public void LaserShot(List<Piece> pieceList)
    {
        Dispose();

        var startPiece = pieceList.FirstOrDefault(p => p.PieceData.PieceType == PieceType.Start);
        if(startPiece != null)
        {
            var sp = startPiece.GetComponent<StartPiece>();

            var laser = CreateLaser(sp, sp.LasetDirection);
            var boardPosition = startPiece.PieceData.BoardPosition;

            if (LaserBeamExecute(pieceList, laser, ref boardPosition))
            {
            }
            else
            {
                // 計算終了
            }
        }

    }

    private bool LaserBeamExecute(List<Piece> pieceList, Laser laser, ref Vector2Int pos)
    {
        var piece = SearchHitPiece(pieceList, laser.LaserDirectionValue, ref pos);
        if (piece != null)
        {
            var laserCollision = piece.LaserCollision(laser);

            // 通過を見つけた時は次へ
            if (laserCollision == Laser.LaserDirection.None)
            {
                return LaserBeamExecute(pieceList, laser, ref pos);
            }
            else if(laserCollision == Laser.LaserDirection.GUARD)
            {
                // ガードが返ってきた時はそこで止める
                laser.SetLaserEnd(piece);

                if(piece.PieceData.PieceType == PieceType.Goal)
                {
                    AudioManager.Instance.PlaySE("robot-startup1");
                    piece.OnGoal();
                    if(_onGameClearEvent != null)
                    {
                        _onGameClearEvent.Invoke();
                    }
                }
                return true;
            }
            else
            {
                // ここが来たら反射している
                laser.SetLaserEnd(piece); // このレーザーはここまで
                
                // 次のレーザーを生成する
                var nextLaser = CreateLaser(piece , laserCollision);
                var nextPos = piece.PieceData.BoardPosition;
                return LaserBeamExecute(pieceList, nextLaser, ref nextPos);
            }
        }

        laser.SetLaserOutOfBounds();
        // 見つからなかったらもう画面外に行く
        return false;
    }

    // 画面端に到達するかpiece見つかるまで直進する
    private Piece SearchHitPiece(List<Piece> pieceList, Laser.LaserDirection direction, ref Vector2Int pos)
    {
        NextPos(direction, ref pos);

        var x = pos.x;
        var y = pos.y;

        var nextPiece = pieceList.FirstOrDefault(p => p.IsBoardPosition(x , y));
        if (nextPiece != null)
        {
            return nextPiece;
        }
        else
        {
            if(
                (x > -1 && x < Define.BOARD_COUNT_X) &&
                (y > -1 && y < Define.BOARD_COUNT_Y))
            {
                return SearchHitPiece(pieceList, direction, ref pos);
            }
            else
            {
                // 画面端到達
                return null;
            }
        }
    }


    private void NextPos(Laser.LaserDirection direction , ref Vector2Int pos)
    {
        switch (direction)
        {
            case Laser.LaserDirection.LEFT:
                pos.x -= 1;
                break;
            case Laser.LaserDirection.RIGHT:
                pos.x += 1;
                break;
            case Laser.LaserDirection.UP:
                pos.y += 1;
                break;
            case Laser.LaserDirection.DOWN:
                pos.y -= 1;
                break;
        }
    }
}

こんな感じのプログラムでレーザーの反射を計算しています。
思っていたよりシンプルでしたでしょうか?
まぁ、そんなに難しいものではない、と思っていただければと。

最悪の事態

Unity2020.2.0f1で開発していたんですが、
WebGLの出力が上手くいかず冷や汗を書きました。
これに気づいたのが日曜日の18:00。あと2時間しかありません。

慌てました。

昔使ってた2019.3.0f1にプロジェクトを差し替えて難を逃れ
この時点で19:00。ラスト1時間でアイコンとか作ったり、
チュートリアル差し込んだり、ツイートしたり。
本当にギリギリでした。

感想

今回も遅刻せず提出する事が出来ました。
もうツイート機能もランキング機能も要りません。
あっても無くても変わらないので。

時間がもうちょっとあったら思いっきりR-18な作品出して
レギュレーション違反でアカBANされたりしたいです。
今回描いたキャラを修正する時間がなくてもうちょっと描きなおしたかったなぁ。と。

ではまた次回(?)。
お疲れ様でした。