Closed23

思い立ってテトリス (→ ワールドルールの PDF が良かった)

toyboot4etoyboot4e

テトリス配信が面白かったので、簡易テトリスを作ってみます。
単にブロックが落ちてくるだけじゃなくて、 T-spin みたいな技がキマってくれると嬉しいのですが。

toyboot4etoyboot4e

情報収集

【テトリス】初心者脱却への道 『最適化について』 | テトリス開幕テンプレ まとめ

  • ブロックの形 7 種類 (I, O, T, J, L, S, Z)
  • ブロックの回転 (回転の中心)
  • ブロックの出現時向き

テトリスの作り方 | 2dgames.jp

  • ブロックは 4x4 領域に収まる。 4x4 の配列または Vec<(x, y)> の形で持てばいい
  • 回転は 4 パタンなので、回転後の形も定義しておけば良い

TODO: 着地後にブロックを操作する (滑らせる/回転させる) 猶予時間

toyboot4etoyboot4e

ブロックの定義

define_shape! マクロを作ってブロックの形を定義します。

マクロの使い方

冗長に記述しました:

/// 1 つの形 (4 方向分のデータ)
#[derive(Debug)]
pub struct ShapeData {
    data: [[bool; 16]; 4],
}

/// I, O, T, J, L, S, Z 形の定義
#[rustfmt::skip]
static SHAPE_DEF: [ShapeData; 7] = [
    // I
    define_shape! {
        // Rot0
        [
            . . . .
            # # # #
            . . . .
            . . . .
        ],
        // Rot90
        [
            . . # .
            . . # .
            . . # .
            . . # .
        ],
        // Rot180
        [
            . . . .
            . . . .
            # # # #
            . . . .
        ],
        // Rot270
        [
            . # . .
            . # . .
            . # . .
            . # . .
        ],
    },

    // O
    define_shape! {
        [
            . . . .
            . # # .
            . # # .
            . . . .
        ],
        [
            . . . .
            . # # .
            . # # .
            . . . .
        ],
        [
            . . . .
            . # # .
            . # # .
            . . . .
        ],
        [
            . . . .
            . # # .
            . # # .
            . . . .
        ],
    },

    // ~~~~
};

マクロ定義

再帰的な形 ($( $rest:tt )*) を避けて書いてみました:

macro_rules! terrain_one {
    ( . ) => {
        false
    };
    ( # ) => {
        true
    };
}

macro_rules! terrain_array {
    ( [ $($x:tt)* ] ) => {
        [
            $( terrain_one!($x), )*
        ]
    }
}

/// Takes terrain arrays as input
macro_rules! define_shape {
    ( $($terrain_array:tt,)* ) => {
        ShapeData {
            data: [
                $(
                    terrain_array!($terrain_array),
                )*
            ],
        }
    }
}
toyboot4etoyboot4e

ステージとブロック

テトリスのステージを作り、前回定義したブロックを表示できるようにします。

プレイヤーのブロック表示

Rust はイテレータが強いので簡単でした:

  • 座標系
    左上のセルを (0, 0) にしてみます。

  • 初期位置
    (4, -1) にしてみます。

ステージにブロックを push

O 字形ブロックをステージに書き込んでみました:


そろそろ本物のテトリスを観察したくなります。 T-spin をやりたくてこのスクラップを始めたわけですが、そういえば同僚が Switch を持ってました。交渉しよう!

toyboot4etoyboot4e

重力、移動、死

重力を実装しました。また、左右キーで x 方向に動けるようにしてみました。衝突判定はまだありません:

ブロックがステージの左端を超えた瞬間、オーバーフローで死にました。

例: -1i32 as u32 + 100 はオーバーフロー

境界チェックを追加します。 2dgames.jp の通り番兵を追加する (ステージを透明なブロックで囲うことで、ステージ外を特別視する必要が無くなる) のも良さそうです。

前者が僕の好みで、文脈が無くてもコードが読めると思います。

toyboot4etoyboot4e

這いずり回ろうぜ

下キー押下でスピードアップ、ステージの底辺で停止します。

まだブロックには当たり判定が無いためプレイできません。回転時にめり込んだら浮き上がってくる処理も必要そうです。

toyboot4etoyboot4e

衝突、着地、次手

ブロックを積めるようになりました。

toyboot4etoyboot4e

回転ってこういうものだっけ?

ブロックの種類によっては、 X 座標が回転中にブレるのですね:

  • 着地中に回転させた場合は、常に地面にへばりつくようなイメージがあります。この間は底辺の Y 座標がブレません。
  • 着地中でなければ、回転先のブロックに弾かれるイメージがあります。
toyboot4etoyboot4e

行くな、奈落へ!

僕のフレームワークには欠陥があり、 FPS がガタ落ちすることがあります。この間は DeltaTime が大きくなり、 1 回の update でブロックが進む距離が増えます。

ブロックが 1 フレームに 1 セル以上落下する場合、ブロックがステージの底をすり抜けてしまいました。考え得る対策としては、

  • 1 フレームに移動可能な y 座標を、最大 0.999 にする。
    • Pro: FPS がガタ落ちした時もすり抜けを防止できます。
    • Con: 落下の最大速度は 1 セル / フレーム に制限されます。
  • 落下後に埋まったら、埋まらないところまで巻き戻す
    • Pro: 理想的なシミュレーションができます。
    • Con: 思いつかない……

実装が楽だったため、一旦前者を採用しました。

toyboot4etoyboot4e

先駆者のリサーチ

テトリスの仕様が公開されているようですね。

ハードドロップ (下キー) とソフトドロップ (Space) が別操作だと知りませんでした……。落下時の速度などにも基準があるようで、悩まずに済みそうです。特に 回転時補正の仕様 (SRS) が (一部間違っているそうですが) 分かるのがありがたいです。

toyboot4etoyboot4e

虚無の時間、仕様確認

play-tetris で完成版を遊ぶことができます。作る必要ないじゃんよ!

着地時の回転の仕様が思っていたのと違いました。ブロックの種類によっては、着地後に回転すれば浮遊できます。ただし浮遊の回数か着地時の回転回数に制限がかかっているようで、永遠に浮かび続けることはできませんでそた。

Tetris Guideline PDF

この話は Tetris GuidelinePDF に載っていました。

In extended Placement, a tetrimino gets 15 left/right movements or rotations before it Locks down, regardless of the time left on the Lock down timer. However, if the tetrimino falls one row below the lowest row yet reached, this counter is reset. In all other cases, it is not reset.

というか 翻訳版 にもちゃんと載っていますね。そうかぁ……

最後にテトリスガイドラインのチェックリストを作成して、完成度を確かめるところまでやりたいです。

toyboot4etoyboot4e

我々は "上昇" するッ

Super Rotation System (回転時の位置補正) を実装しました:

壁際でも回転可能です! でも無限上昇できてしまいました。たぶん落下速度と回転頻度に修正を加えたらいいかな……。

SSR を実装したおかげで、 T-Spin を含む 回転入れ が可能になったはずです。次回、テトリスをプレイ?!

toyboot4etoyboot4e

消滅、バッグ制、足りないものたち

一番足りないのはプレイスキルです。

toyboot4etoyboot4e

予測、ゴースト、バグ

次のブロックや着地予測を表示して、少し見やすくなりました:

toyboot4etoyboot4e

敗北条件

Tetris Guideline から引いてきた:

a. 攻撃により最上のブロックがステージを越えようとしたとき
b. プレイヤーが skyline (下から 20 行目) よりも上にブロックを固定したとき
c. 次のブロックのセルがステージ上ブロックによって塞がれたとき

画面上の変化は無いけれど、敗北状態でゲームが停止するようになった。


ゲームオーバー時は実行しないシステムがあって、システム関数にガードをつけたい。ガード付きの関数を返す高階関数を作りたかったけれど、今のところコンパイルできない。

toyboot4etoyboot4e

(ECS) システムのガード

ゲームオーバー時は実行しない:

world.run_ex(guarded(ink_teris::delete_system));

guarded 関数:

/// Retruns a system that runs only when the game is running
fn guarded<P, Ret>(f: impl Copy + ExclusiveSystem<P, Ret>) -> impl FnMut(&mut World) {
    move |world: &mut World| {
        if world.res::<ink_tetris::mdl::State>().is_game_over {
            return;
        }

        world.run_ex(f.clone());
        // the output is discarded
    }
}
toyboot4etoyboot4e

ゴーストの見た目

アウトライン表示に変えてみた。

toyboot4etoyboot4e

zako.png

バグは直したはずなのだけれど、こんなガタガタなのはおかしいよなぁ

なぁ 僕は T-spin がしたいだけなんだ

toyboot4etoyboot4e

ついにテトリスの広告が流れ始めた

やめろ その神プレイは俺に効く

toyboot4etoyboot4e

凍結

未完ですが満足したので閉じます。

恐らく T-Spin も実装できているはず! ただテトリスの腕が足りなかった……

このスクラップは2022/07/04にクローズされました