十字方向に動くセレクトUIの実装


ドラクエなどでよくあるこういうやつです。

今回GameMaker Studio2で実装したので、 JS風のGMLという言語で実装していますが、
特殊な機能はほとんど用いていないのでどんな言語/ゲームエンジンでも応用が効くと思います。

このロジックでは、次に指し示すべきindex位置を取得し、
それを元に描画すべきページ数を計算するという流れになっています。


// ここら辺はキーを押した判定のための変数です。
keyLeft = keyboard_check_pressed(ord("A")) || keyboard_check_pressed(vk_left);
keyRight = keyboard_check_pressed(ord("D")) || keyboard_check_pressed(vk_right);
keyUp = keyboard_check_pressed(ord("W")) || keyboard_check_pressed(vk_up);
keyDown = keyboard_check_pressed(ord("S")) || keyboard_check_pressed(vk_down);
keySpace = keyboard_check_pressed(vk_space);


pressX = keyRight - keyLeft;
pressY = keyDown - keyUp;
itemCount = array_length(selectList);

selectCount = selectIndex - startIndex;
posXY = [selectCount  % 2, floor(selectCount / 2)]; // xy座標

// x軸
if(pressX != 0) {
    selectIndex += pressX;

    // 次のページが存在しない場合
    if(selectIndex > itemCount - 1) selectIndex = 0;
    // 前のページが存在しない場合
    if(selectIndex < 0) selectIndex = itemCount - 1;
}


// y軸
if(pressY != 0 && itemCount > 2) {
    var _prev = selectIndex;
    var _nowPage = floor(_prev / 4);
    selectIndex += pressY * 2;

    // 次のページが存在しない場合
    if(selectIndex > itemCount - 1) {
        // アイテム数が奇数の場合かつ1つ前が存在すればそれを返す
        if(itemCount % 2 == 1 && itemCount - 1 >= selectIndex - 1) {
            selectIndex -= 1;
        } else {
            selectIndex = posXY[0];
        }
    }
    // 前のページが存在しない場合
    if(selectIndex < 0) {
        // アイテム数が奇数の場合は最後尾を返す
        if(itemCount % 2 == 1) {
            selectIndex = itemCount - 1;
        } else {
            selectIndex = (itemCount - 1)  - 1 + posXY[0];

        }
    }
}

// ページ位置計算
startIndex = floor((selectIndex) / 4) * 4;

あとは他コンポーネントでstartIndex(ページ位置の始まりのindex位置)selectIndex(現在洗濯中の項目のindex)を用いて画面描画すればOKです。
選択した項目はitem[selectIndex]などで取得すればいいですね。

ポイント

奇数アイテムに注意

奇数アイテムだと、y軸方向(上か下)には偶数の場合と同様には移動することができないので注意です。

// アイテム数が奇数の場合かつ1つ前が存在すればそれを返す
if(itemCount % 2 == 1 && itemCount - 1 >= selectIndex - 1) {
    selectIndex -= 1;
} else {
    selectIndex = posXY[0];
}

上記は先頭から戻る場合(上に進む場合)に、x座標が0,1どちらであっても一番最後のindexを示すように調整しています。
ユーザ的にその方が自然に感じるかと思いそう実装してみました。

下に進んだ場合はその場合も2つ戻った先の1つ前が存在すればそれを返すようにしています。
こうすることで、下が空白だった場合にも違和感のない挙動が実現できます。

// アイテム数が奇数の場合かつ1つ前が存在すればそれを返す
if(itemCount % 2 == 1 && itemCount - 1 >= selectIndex - 1) {
    selectIndex -= 1;
} else {
    selectIndex = posXY[0];
}

まとめ

実は最初の頃は、「ページ遷移が起こるか否か」を一番最初の分岐に持ってきてしまい、
ロジックが非常に複雑になってしまいましたが、選択するのindex基準でリファクタリングしたところ非常にシンプルに実装することができました。
怪しい雲行きになってきた時は見方やロジックの根本部分を見直すのがとても大切だと改めて学びました🐕

また、上記コードだと必要ない工程まで毎度走ってしまうので、
パフォーマンス調整するなら一部を関数化してreturnするなどの調整が必要だと思います...!(今回手を抜いてしまいました)