今回は前回作った削除の演出にアニメーションをつけてみようと思います。
■前回の内容
Unityでパズドラ風3Matchパズルを作る_その2_演出編1 - スマゲ
■本編の完成メージ
※Gifだとわかりづらいですが、ピースが移動するときもアニメーションをしています
■コード確認用リポジトリ
github.com
■環境
Mac OS Sierra 10.12.4
Unity 5.6.0 f3
言語 C#
■実装
ここからはその2のつづきです。
■アニメーション定義構造体の実装
スクリプト[AnimData.cs]を作成し以下のように記述します。
ここではアニメーションするゲームオブジェクト、移動先、移動にかける時間を定義できます。
using UnityEngine; // アニメーション定義保持用構造体 public struct AnimData { public GameObject targetObject; public Vector3 targetPosition; public float duration; public AnimData(GameObject target, Vector3 pos, float dur) { targetObject = target; targetPosition = pos; duration = dur; } }
■アニメーションクラスの実装
スクリプト[MoveTween.cs]を作成、以下のように記述します。
using System; using UnityEngine; // 移動アニメーションクラス public class MoveTween : MonoBehaviour { public Vector3 fromPosition; public Vector3 toPosition; public float duration; private bool isTween; private float elapsedTime; private Action endCallBack; //------------------------------------------------------- // MonoBehaviour Function //------------------------------------------------------- // アニメーションの更新処理 private void Update(){ if (!isTween) { return; } // アニメーション開始時からの経過時間 elapsedTime += Time.deltaTime; if (elapsedTime >= duration) { // アニメーションの終了処理 transform.position = toPosition; isTween = false; if (endCallBack != null) { endCallBack(); } Destroy(this); return; } // var moveProgress = elapsedTime / duration; transform.position = Vector3.Lerp(fromPosition, toPosition, moveProgress); } //------------------------------------------------------- // Public Function //------------------------------------------------------- // アニメーション開始処理 public void DoTween(Vector3 from, Vector3 to, float dur, Action endcb) { fromPosition = from; toPosition = to; duration = dur; endCallBack = endcb; transform.position = from; elapsedTime = 0; isTween = true; } }
■アニメーションの管理クラス
スクリプト[TweenAnimationManager.cs]を作成、以下のように記述します。
using System.Collections; using System.Collections.Generic; using UnityEngine; // アニメーションの管理クラス public class TweenAnimationManager : MonoBehaviour { private Queue<List<AnimData>> animQueue = new Queue<List<AnimData>>(); bool isTween; int tweenAnimationCount; int endAnimCount; //------------------------------------------------------- // MonoBehaviour Function //------------------------------------------------------- // アニメーション実行処理 private void Update() { if (isTween) { return; } if (animQueue.Count > 0) { endAnimCount = 0; isTween = true; var queue = animQueue.Dequeue(); tweenAnimationCount = queue.Count; foreach (var data in queue) { var tween = data.targetObject.AddComponent<MoveTween>(); tween.DoTween(data.targetObject.transform.position, data.targetPosition, data.duration, () => { endAnimCount++; if (tweenAnimationCount == endAnimCount) { isTween = false; } }); } } } // アニメーションのセット処理 public void AddListAnimData(List<AnimData> animData) { animQueue.Enqueue(animData); } }
■アニメーション部分の説明
まずピースの生成時と移動時に必要な情報を[AnimData]のListとして作成し、[TweenAnimationManager.cs]の[AddListAnimData]の引数にして呼び出します。
[AddListAnimData]では[Queue]に作成したしたアニメーションを登録し、順番に実行します。[Update]で順番待ちをしているため、[Queue]の1要素を1単位としてアニメーションを実行し、それが終わったら[Queue]を確認し、もし要素がまだあればなくなるまで、アニメーションを再生します。
■アニメーションの適用
[Board.cs]を開いて以下のように編集します。
変更点は以下になります。
・盤面初期化時にピースが上から落ちてくるように変更
・ピース操作時にアニメーションするように変更
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; // 盤面クラス public class Board : MonoBehaviour { // const private const float FillPieceDuration = 0.2f; private const float SwitchPieceCuration = 0.02f; // serialize field. [SerializeField] private GameObject piecePrefab; [SerializeField] private TweenAnimationManager animManager; // private. private Piece[,] board; private int width; private int height; private int pieceWidth; private int randomSeed; private Vector2[] directions = new Vector2[] { Vector2.up, Vector2.down, Vector2.right, Vector2.left }; private List<AnimData> fillPieceAnim = new List<AnimData>(); private List<Vector2> pieceCreatePos = new List<Vector2>(); //------------------------------------------------------- // Public Function //------------------------------------------------------- // 特定の幅と高さに盤面を初期化する public void InitializeBoard(int boardWidth, int boardHeight) { width = boardWidth; height = boardHeight; pieceWidth = Screen.width / boardWidth; board = new Piece[width, height]; for (int i = 0; i < boardWidth; i++) { for (int j = 0; j < boardHeight; j++) { CreatePiece(new Vector2(i, j)); } } animManager.AddListAnimData(fillPieceAnim); } // 入力されたクリック(タップ)位置から最も近いピースの位置を返す public Piece GetNearestPiece(Vector3 input) { var x = Mathf.Min((int)(input.x / pieceWidth), width - 1); var y = Mathf.Min((int)(input.y / pieceWidth), height - 1); return board[x, y]; } // 盤面上のピースを交換する public void SwitchPiece(Piece p1, Piece p2) { // 位置を移動する var animList = new List<AnimData>(); animList.Add(new AnimData(p1.gameObject, GetPieceWorldPos(GetPieceBoardPos(p2)), SwitchPieceCuration)); animList.Add(new AnimData(p2.gameObject, GetPieceWorldPos(GetPieceBoardPos(p1)), SwitchPieceCuration)); animManager.AddListAnimData(animList); // 盤面データを更新する var p1BoardPos = GetPieceBoardPos(p1); var p2BoardPos = GetPieceBoardPos(p2); board[(int)p1BoardPos.x, (int)p1BoardPos.y] = p2; board[(int)p2BoardPos.x, (int)p2BoardPos.y] = p1; } // 盤面上にマッチングしているピースがあるかどうかを判断する public bool HasMatch() { foreach (var piece in board) { if (IsMatchPiece(piece)) { return true; } } return false; } // マッチングしているピースを削除する public IEnumerator DeleteMatchPiece(Action endCallBadk) { foreach (var piece in board) { if (piece != null && IsMatchPiece(piece)) { var pos = GetPieceBoardPos(piece); DestroyMatchPiece(pos, piece.GetKind()); yield return new WaitForSeconds(0.5f); } } endCallBadk(); } // ピースが消えている場所を詰めて、新しいピースを生成する public IEnumerator FillPiece(Action endCallBack) { // アニメーション管理リストとピース生成位置保持リストを初期化する fillPieceAnim.Clear(); pieceCreatePos.Clear(); for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { FillPiece(new Vector2(i, j)); } } // アニメーションを再生する animManager.AddListAnimData(fillPieceAnim); yield return new WaitForSeconds(1f); endCallBack(); } //------------------------------------------------------- // Private Function //------------------------------------------------------- // 特定の位置にピースを作成する private void CreatePiece(Vector2 position) { // ピースの位置を求める var piecePos = GetPieceWorldPos(position); // ピースの生成位置を求める var createPos = new Vector2(position.x, height); while (pieceCreatePos.Contains(createPos)) { createPos += Vector2.up; } pieceCreatePos.Add(createPos); var pieceCreateWorldPos = GetPieceWorldPos(createPos); // 生成するピースの種類をランダムに決める var kind = (PieceKind)UnityEngine.Random.Range(0, Enum.GetNames(typeof(PieceKind)).Length); // ピースを生成、ボードの子オブジェクトにする var piece = Instantiate(piecePrefab, pieceCreateWorldPos, Quaternion.identity).GetComponent<Piece>(); piece.transform.SetParent(transform); piece.SetSize(pieceWidth); piece.SetKind(kind); // 盤面にピースの情報をセットする board[(int)position.x, (int)position.y] = piece; // アニメーションのセット fillPieceAnim.Add(new AnimData(piece.gameObject, piecePos, FillPieceDuration)); } // 盤面上の位置からピースオブジェクトのワールド座標での位置を返す private Vector3 GetPieceWorldPos(Vector2 boardPos) { return new Vector3(boardPos.x* pieceWidth + (pieceWidth / 2), boardPos.y* pieceWidth + (pieceWidth / 2), 0); } // ピースが盤面上のどの位置にあるのかを返す private Vector2 GetPieceBoardPos(Piece piece) { for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if (board[i, j] == piece) { return new Vector2(i, j); } } } return Vector2.zero; } // 対象のピースがマッチしているかの判定を行う private bool IsMatchPiece(Piece piece) { // ピースの情報を取得 var pos = GetPieceBoardPos(piece); var kind = piece.GetKind(); // 縦方向にマッチするかの判定 MEMO: 自分自身をカウントするため +1 する var verticalMatchCount = GetSameKindPieceNum(kind, pos, Vector2.up) + GetSameKindPieceNum(kind, pos, Vector2.down) + 1; // 横方向にマッチするかの判定 MEMO: 自分自身をカウントするため +1 する var horizontalMatchCount = GetSameKindPieceNum(kind, pos, Vector2.right) + GetSameKindPieceNum(kind, pos, Vector2.left) + 1; return verticalMatchCount >= GameManager.MachingCount || horizontalMatchCount >= GameManager.MachingCount; } // 対象の方向に引数で指定したの種類のピースがいくつあるかを返す private int GetSameKindPieceNum(PieceKind kind, Vector2 piecePos, Vector2 searchDir) { var count = 0; while (true) { piecePos += searchDir; if (IsInBoard(piecePos) && board[(int)piecePos.x, (int)piecePos.y].GetKind() == kind) { count++; } else { break; } } return count; } // 対象の座標がボードに存在するか(ボードからはみ出していないか)を判定する private bool IsInBoard(Vector2 pos) { return pos.x >= 0 && pos.y >= 0 && pos.x < width && pos.y < height; } // 特定のピースのが削除されているかを判断し、削除されているなら詰めるか、それができなければ新しく生成する private void FillPiece(Vector2 pos) { var piece = board[(int)pos.x, (int)pos.y]; if (piece != null && !piece.deleteFlag) { // ピースが削除されていなければ何もしない return; } // 対象のピースより上方向に有効なピースがあるかを確認、あるなら場所を移動させる var checkPos = pos + Vector2.up; while (IsInBoard(checkPos)) { var checkPiece = board[(int)checkPos.x, (int)checkPos.y]; if (checkPiece != null && !checkPiece.deleteFlag) { fillPieceAnim.Add(new AnimData(checkPiece.gameObject, GetPieceWorldPos(pos), FillPieceDuration)); board[(int)pos.x, (int)pos.y] = checkPiece; board[(int)checkPos.x, (int)checkPos.y] = null; return; } checkPos += Vector2.up; } // 有効なピースがなければ新しく作る CreatePiece(pos); } // 特定のピースがマッチしている場合、ほかのマッチしたピースとともに削除する private void DestroyMatchPiece(Vector2 pos, PieceKind kind) { // ピースの場所が盤面以外だったら何もしない if (!IsInBoard(pos)) { return; } // ピースが無効であったり削除フラグが立っていたりそもそも、種別がちがうならば何もしない var piece = board[(int)pos.x, (int)pos.y]; if (piece == null || piece.deleteFlag || piece.GetKind() != kind) { return; } // ピースが同じ種類でもマッチングしてなければ何もしない if (!IsMatchPiece(piece)) { return; } // 削除フラグをたてて、周り4方のピースを判定する piece.deleteFlag = true; foreach (var dir in directions) { DestroyMatchPiece(pos + dir, kind); } // ピースを削除する Destroy(piece.gameObject); } }
■実行
■その4_演出編3
ピースの移動と補充のアニメーションができましたが、自分の操作しているピースがわかりづらいのでそこらへんを改善していきたいと思います。
Unityでパズドラ風3Matchパズルを作る_その4_演出編3 - スマゲ
■関連リンク
Unityでパズドラ風3Matchパズルを作る_その1_ロジック編 - スマゲ
Unityでパズドラ風3Matchパズルを作る_その2_演出編1 - スマゲ
Unityでパズドラ風3Matchパズルを作る_その4_演出編3 - スマゲ
Unityでパズドラ風3Matchパズルを作る_その5_演出編4 - スマゲ
Unityでパズドラ風3Matchパズルを作る_その6_ステータス表示編 - スマゲ