コード置き場「リズムSTG #1 – #30」

コード置き場:リズムSTG(#1-#30)
スポンサーリンク

【リズムSTG制作】の動画内で表示されるコード

サムネイルをクリックすると、Youtubeの動画ページに移動します。

#2:音楽と同期したアニメーションを作ってみる

MIDI Animation Trackを使用して、音楽と同期したアニメーションを作成します。
サムネイル#2
manifest.json
{
  "scopedRegistries": [
    {
      "name": "Keijiro",
      "url": "https://registry.npmjs.com",
      "scopes": [ "jp.keijiro" ]
    }
  ],
  "dependencies": {
    "jp.keijiro.klak.timeline.midi": "1.0.5",
    ...

#3:音楽に合わせてオブジェクトを生成してみる

MIDI Signal ReceiverでNote On Eventにオブジェクトを生成する関数を登録します。
サムネイル#3
ノートを生成
// 生成するノート(1)
[SerializeField] GameObject noteRed;

// 生成するノート(2)
[SerializeField] GameObject noteYel;


public void NoteOnC()
{
    // C(ド)の音でノートを生成
    Instantiate(noteRed, new Vector2(7, 3), transform.rotation);
}

public void NoteOnD()
{
    // D(レ)の音でノートを生成
    Instantiate(noteYel, new Vector2(7, 2), transform.rotation);
}
ノートの移動と削除
// ノートの移動速度
[SerializeField] float speed;


void Update()
{
    // 左に移動
    transform.position += Vector3.left * speed * Time.deltaTime;
}

void OnTriggerExit2D(Collider2D col)
{
    // 判定ラインを通過したら
    if (col.CompareTag("Judge"))
    {
        // 爆発エフェクト
        // Explosion();
        // 削除
        Destroy(gameObject);
    }
}

#5:ランダム要素の追加

乱数で敵ユニットの配置や行動が毎回変化するようにします。
サムネイル#5
ランダムに敵弾を選択
// 敵弾
[SerializeField] GameObject[] bullets;


void Shot()
{
    // 配列の中からランダムに選択する
    int index = Random.Range(0, bullets.Length);

    // 乱数で選ばれた敵弾を生成
    Instantiate(bullets[index], transform.position, Quaternion.identity);
}

void Initialize()
{
    // 現在時刻のミリ秒でシード値を設定
    Random.InitState(DateTime.Now.Millisecond);
}
配列のシャッフル
// 生成するノート
[SerializeField] GameObject noteRed;

// 敵ユニットの生成位置
Vector2[] array = new [] {new Vector2(7, 3), new Vector2(7, 2), ... };


void Start()
{
    // 並べ替えを実行
    Shuffle(array);
}

// 配列(Vector2型)のシャッフル
void Shuffle (Vector2[] deck)
{
    for (int i = 0; i < deck.Length; i++)
    {
        var temp = deck[i];
        int randomIndex = Random.Range(i, deck.Length);
        deck[i] = deck[randomIndex];
        deck[randomIndex] = temp;
    }
}

public void NoteOnC()
{
    // シャッフル後の配列で敵ユニットを生成
    Instantiate(noteRed, array[0], transform.rotation);
}

#6:プレイヤーのカスタマイズ

Sprite Library Assetを使用して、プレイヤーが乗っている箒をカスタマイズできるようにします。
サムネイル#6R
ドロップダウンでパーツを選択
using TMPro;
using UnityEngine.U2D.Animation;
--------------------------------

SpriteResolver spriteResolver1;
// ドロップダウン
[SerializeField] TMP_Dropdown dropdown1;

void Start()
{
    // SpriteResolver取得
    spriteResolver1 = GameObject.FindWithTag("Broom1").GetComponent<SpriteResolver>();
}

public void CustomParts1()
{
    // 一番上の選択肢が選ばれたら
    if (dropdown1.value == 0)
    {
        // 選択肢のスプライトに切り替える
        spriteResolver1.SetCategoryAndLabel("broom1", "broom_1A");
    // 二番目の選択肢が選ばれたら
    } else if (dropdown1.value == 1) {
    ...
    }
}

#7:オブジェクトプール

Unityの標準機能に追加されたオブジェクトプールを使用して、プレイヤーのショットを管理します。
サムネイル#7R
ObjectPool
using UnityEngine;
using UnityEngine.Pool;
-------------------------

// オブジェクトプールで管理するアイテム
[SerializeField] GameObject playerBullet;

// ショットを管理するプール
private ObjectPool<GameObject> bulletPool;

void Awake()
{
    bulletPool = new ObjectPool<GameObject>(
        CreatePoolObject,
        OnGetFromPool,
        OnReleaseToPool,
        OnDestroyPoolObject,
        collectionCheck: true,
        defaultCapacity: 5,
        maxSize: 10
    );
}

private GameObject CreatePoolObject()
{
    // プレイヤーのショットを生成
    return Instantiate(playerBullet);
}

private void OnGetFromPool(GameObject target)
{
    // オブジェクトを表示
    target.SetActive(true);
}

private void OnReleaseToPool(GameObject target)
{
    // オブジェクトを非表示
    target.SetActive(false);
}

private void OnDestroyPoolObject(GameObject target)
{
    // オブジェクトを破棄
    Destroy(target);
}

public GameObject GetPoolBullet(Vector3 shotPos)
{
    // プールからショットを取り出す
    GameObject obj = bulletPool.Get();
    obj.transform.position = shotPos;
    return obj;
}

public void ReleaseBullet(GameObject obj)
{
    // ショットをプールに戻す
    bulletPool.Release(obj);
}
プールから取り出す・返却するサンプル
[SerializeField] Manager manager;

Vector3 shotPosition;

void Update()
{
    // ショットキーが入力されている場合
    if (fire.ReadValue<float>() > 0)
    {
        // プールから取り出す
        manager.GetPoolBullet(shotPosition);
    }
}

--------------------------------------------
// Releaseエリアに接触したらプールに返却
void OnTriggerEnter2D(Collider2D col)
{
    manager.ReleaseBullet(col.gameObject);
}

#8:スワイプでプレイヤーを操作

Input Systemを使用して、スワイプ操作を実装します。
サムネイル#8R
スワイプ操作のサンプル
using UnityEngine;
using UnityEngine.InputSystem;
-------------------------------

// スワイプの開始位置
Vector2 startPos;

// 現在位置
Vector2 currentPos;

// プレイヤーの移動方向
Vector2 direction;

// 移動量の調整
float adjustValue;

// ワールド座標に変換するカメラ
Camera cam;

void Start()
{
    cam = Camera.main;
}

void Update()
{
    // 画面に触れた瞬間
    if (Pointer.current.press.wasPressedThisFrame)
    {
        startPos = Pointer.current.position.ReadValue();
    }

    // 画面に触れている間
    if (Pointer.current.press.isPressed)
    {
        currentPos = Pointer.current.position.ReadValue();
        // スクリーン座標をワールド座標に変換
        var worldPosS = cam.ScreenToWorldPoint(startPos);
        var worldPosC = cam.ScreenToWorldPoint(currentPos);
        var vector = worldPosC - worldPosS;
        // スワイプ量に応じてプレイヤーを移動させる
        direction = new Vector2(vector.x, vector.y);
        direction *= adjustValue;
        // ベクトルの起点を更新
        startPos = currentPos;
    }

    // 画面から指が離れたら
    if (Pointer.current.press.wasReleasedThisFrame)
    {
        // プレイヤーを停止
        direction = Vector2.zero;
    }

    // プレイヤーを移動させる処理
    transform.position += direction * speed * Time.deltaTime;
}

#9:コンボ数を表示する

コンボ数を表示するUIを作成します。
サムネイル#9R
コンボ数を表示(シンプル)
using UnityEngine;
using TMPro;
------------------------------

// コンボ数を表示するCanvasGroup
[SerializeField] CanvasGroup grCombo;
[SerializeField] TextMeshProUGUI comboText;

// コンボ数
private int combo;

// 経過時間
private float elapsedTime;

// コンボ数を表示する時間
private float displayTime = 1.0f;

// コンボ数が表示されたか
private bool isCombo;

void Update()
{
    // コンボ数が表示されたら計測開始
    if (isCombo == true)
    {
        elapsedTime += Time.deltaTime;
        // 一定時間経過したらコンボ数を非表示
        if (elapsedTime >= displayTime)
        {
            grCombo.alpha = 0f;
            isCombo = false;
        }
    }
}

public void ShowCombo()
{
    // コンボ数をセットして表示
    comboText.SetText("{0}", combo);
    grCombo.alpha = 1f;
    // 経過時間をリセットして計測開始
    elapsedTime = 0f;
    isCombo = true;
} 
コンボ数を表示(アニメーションあり)
using UnityEngine;
using DG.Tweening;
using TMPro;
------------------------------

// コンボ数を表示するCanvasGroup
[SerializeField] CanvasGroup grCombo;
[SerializeField] TextMeshProUGUI comboText;

// テキストのRectTransform
[SerializeField] RectTransform rectCombo;

// コンボ数
private int combo;

// 通常時のアニメーション
Sequence sequence1;

// 特定のコンボ数で再生するアニメーション
Sequence sequence2;

void Start()
{
    // 通常時のアニメーション
    sequence1 = DOTween.Sequence()
        .Append(grCombo.DOFade(1f, 0.2f))
        .Join(rectCombo.DOScale(new Vector3(1.8f, 1.8f, 1.0f), 0.2f))
        // コンボ数を表示する時間
        .AppendInterval(0.4f)
        // フェードアウト
        .Append(grCombo.DOFade(0f, 0.4f))
        // アニメーションを再利用する
        .Pause().SetAutoKill(false).SetLink(gameObject);

    // 5Comboごとに再生するアニメーション
    sequence2 = DOTween.Sequence()
        .Append(grCombo.DOFade(1f, 0.2f))
        .Join(rectCombo.DOScale(new Vector3(2.5f, 2.5f, 1.0f), 0.2f))
        // 拡大しながらフェードアウト
        .Append(grCombo.DOFade(0f, 0.4f))
        .Join(rectCombo.DOScale(new Vector3(4.0f, 4.0f, 1.0f), 0.4f))
        .Pause().SetAutoKill(false).SetLink(gameObject);
}

public void ShowComboAnim()
{
    // コンボ数をセットして初期化
    comboText.SetText("{0}", combo);
    rectCombo.localScale = Vector3.one;
    // 5Comboごとのアニメーション
    if (combo % 5 == 0)
    {
        // ひとつ前のアニメーションが再生中であればスキップ
        if (sequence1.IsPlaying()) sequence1.Complete();
        // アニメーション再生
        sequence2.Restart();
    // 通常時のアニメーション
    } else {
        if (sequence2.IsPlaying()) sequence2.Complete();
        sequence1.Restart();
    }
}

#10:スコアのカウントアップアニメーション

DOTweenでカウントアップアニメーションを2種類作成します。
サムネイル#10R
カウントアップ(シンプル)
using UnityEngine;
using DG.Tweening;
using TMPro;
------------------------------

// スコアを表示するText
[SerializeField] TextMeshProUGUI scoreText;

// スコア
private int score;

// 加算前のスコア
private int previousValue;

// カウントアップ中か
private bool isCountUp;

// カウントアップアニメーション
Sequence sequence;

void Update()
{
    // カウントアップのアニメーション中であれば
    if (isCountUp == true)
    {
        // スコア表示を更新する(0埋め)
        scoreText.SetText("{0:000000}", previousValue);
    }
}

public void AddPoint(int point)
{
    // 得点が加算される前の値
    previousValue = score;
    // スコアを更新
    score += point;
    // アニメーションが再生中であればスキップ
    if (isCountUp == true) sequence.Kill(true);
    // カウントアップアニメーション開始
    CountUpAnim();
}

void CountUpAnim()
{
    isCountUp = true;
    sequence = DOTween.Sequence()
        .Append(DOTween.To(() => previousValue, num => previousValue = num, score, 0.5f))
        // 少し待機してから
        .AppendInterval(0.1f)
        // スコア表示の更新を停止
        .AppendCallback(() => isCountUp = false);
}
カウントアップ(縦回転)
using UnityEngine;
using DG.Tweening;
using TMPro;
------------------------------

// スコア
private int score;

// アニメーションが再生中か
private bool isCountUp;

// スコアの各桁の数字
private int[] digits;

// 数字(画像)の位置
[SerializeField] RectTransform rectScore1;
[SerializeField] RectTransform rectScore10;
[SerializeField] RectTransform rectScore100;
[SerializeField] RectTransform rectScore1k;
[SerializeField] RectTransform rectScore10k;
[SerializeField] RectTransform rectScore100k;

private int[] GetAllDigits(int num)
{
    // スコアは最大6桁
    digits = new int[6];
    // 上限を超えている場合は何もしない
    if (num >= 1000000) return digits;
    // すべての桁の数字を取り出す
    for (int i = 0; num > 0; i++)
    {
        int digit = num % 10;
        digits[i] = digit;
        num /= 10;
    }
    return digits;
}

public void CountUpAnim2()
{
    // スコアの各桁の数字を取り出す
    GetAllDigits(score);
    if (isCountUp == false)
    {
        isCountUp = true;
        // 一の位のアニメーション
        rectScore1.DOAnchorPosY(239f, 2.0f)
            .OnComplete(() => {
                // アニメーションが完了したら初期位置に戻す
                rectScore1.localPosition = new Vector3(89f, -241f, 0f);
                isCountUp = false;
            });
        // 十の位のアニメーション
        rectScore10.DOAnchorPosY(239f, 1.5f)
            .OnComplete(() => rectScore10.localPosition = new Vector3(53f, -241f, 0f));
    }

    // 百の位は更新頻度が高いので、スキップ処理で対応
    rectScore100.DOKill(true);
    // 百の位の数字
    switch (digits[2])
    {
        // 数字が表示される位置まで移動
        case 0:
            rectScore100.DOAnchorPosY(239f, 1f)
                // 0の時だけはアニメーション完了後に初期位置に戻す
                .OnComplete(() => rectScore100.localPosition = new Vector3(17f, -241f, 0f));
            break;
        case 1:
            rectScore100.DOAnchorPosY(-193f, 1f);
            break;
        case 2:
            rectScore100.DOAnchorPosY(-145f, 1f);
            break;
        ...
    }

    // スコアが1000点未満であれば終了
    if (score < 1000) return;
    // 千の位の数字
    switch (digits[3])
    {
        // 数字が表示される位置まで移動
        case 0:
            // 千の位に変化がなければアニメーションなし
            if (rectScore1k.localPosition.y == -241f) break;
            rectScore1k.DOAnchorPosY(239f, 1f)
                .OnComplete(() => rectScore1k.localPosition = new Vector3(-19f, -241f, 0f));
            break;
        case 1:
            // 千の位が1だった場合
            if (rectScore1k.localPosition.y == -193f) break;
            rectScore1k.DOAnchorPosY(-193f, 1f);
            break;
        ...
    }

    // 一万の位の処理(digits[4])

    // 十万の位の処理(digits[5])
}
タイトルとURLをコピーしました