古典的なゲーム [テトリス] の完全な開発をゼロから説明します。ロジック全体に必要なコードはわずか 200 行未満です。

クラシック テトリスは忘れられないゲームで、コンピュータのキーボード ショートカットやマウス クリック、または携帯電話のタッチ スクリーンでプレイできます。初期の行数と難易度を設定できます。最後にプレイしたシーンは自動的に保存されます。復元されました。

古典的なゲーム [テトリス] の完全な開発をゼロから説明します。ロジック全体に必要なコードはわずか 200 行未満です。
プロセス全体は、ゲーム ロジック (JS の高度に簡略化されたバージョン) を記述する式を使用して、ZhongTouch ローコード アプリケーション プラットフォーム上で実行されます。
このコースには数学的抽象化に対する高い要件があり、クラウドタッチを基本的にマスターした開発者に適しています。

最終的な効果のデモンストレーション

最初にプレイしてください (登録なし): Zhongtouch ローコード アプリケーション プラットフォーム編集モード

詳しい指導方法については、Bilibili ビデオをご覧ください: [クラウドタッチコース] Tetris_哔哩哔哩_bilibili

ゲームのルール

  1. 幅 10 × 高さ 20 の小さな正方形用のフィールドがあります。

  2. S、Z、L、J、I、O、Tの7文字の形にちなんで名付けられた、4つの小さな正方形で構成される正則図形のセット、計7種類。

  3. ブロックはフィールドの上部にランダムに生成され、特定のルールに従って移動、回転、ドロップ、配置され、ロックされてフィールドに埋められます。会場の 1 つ以上の列が各配置で完全に埋まると、これらの列を構成するすべての小さな正方形が削除され、一定量のポイントが付与されます。消されなかったブロックは常に蓄積され、その後のブロックの配置にさまざまな影響を与えます。

  4. ポイントが増加すると、キューブの落下速度が徐々に速くなり、ゲームの難易度と楽しさが増加します。

  5. ブロックがエリアの一番下に移動したり、他のブロックに落ちて移動できなくなると、そのブロックに固定されてしまいます。除去されていないキューブの積み重ねの高さがフィールドで指定された最大高さを超えると、ゲームが終了します。

4つの州

準備完了(ready)、居場所(fall)、終了(over)、一時停止(pause)。

採点ルール

難易度が高くなるほど移動速度が速くなり、落下や排除のスコアが高くなります。
ドロップスコア: 1 * レベル;
エリミネーションスコア: Math.pow(2, エリミネートされた行数) * レベル * 10;

2次元配列マトリックスを使用してフィールドのレンダリングを制御します

2 次元配列を使用してブロックが配置されている領域全体を表すと、$v.matrix = array(20, array(10)) すべて 0 からなる 10*20 の 2 次元配列行列が得られます。0 はブロックなし、1 はブロックを意味します。
2 つのネストされたデータ コンポーネントを使用してレンダリングします。外側のデータ コンポーネントは$v.matrixデータ ソースとして使用し、内側のレイヤーは外側のレイヤーによって提供されたデータ項目を$xデータ ソース (それ自体 1 次元配列) として上から下に使用します。左から右 ゾーンへの右出力。

たとえば、次の行列の配列は次のとおりです。

[
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
    [1, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [1, 0, 0, 0, 1, 1, 1, 1, 0, 0],
    [1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
    [1, 1, 0, 1, 1, 1, 1, 1, 0, 1]
]

次のようにレンダリングされます:

行列の 1 と図の黒い四角の位置の対応関係に注目してください。数値が 1 の場合、小さな正方形にクラス名を付けます($x ? "c" : "")。ここで、c は黒色を意味します。

テトリスの 7 つの形

プレーヤーは特定の形状のブロックを操作し、4 * 2 の 2 次元配列を使用してブロックの形状をレンダリングします。

[
    [
        [0, 0, 0, 0],
        [1, 1, 1, 1]
    ],
    [
        [0, 0, 1, 0],
        [1, 1, 1, 0]
    ],
    [
        [1, 0, 0, 0],
        [1, 1, 1, 0]
    ],
    [
        [1, 1, 0, 0],
        [0, 1, 1, 0]
    ],
    [
        [0, 1, 1, 0],
        [1, 1, 0, 0]
    ],
    [
        [0, 1, 1, 0],
        [0, 1, 1, 0]
    ],
    [
        [0, 1, 0, 0],
        [1, 1, 1, 0]
    ]
]

$v.blocksそれを変数に保存します。配列内の 1 の形状 (I、L、J、Z、S、O、T) に注目してください。

キーボードとクリックのコントロール

プレーヤーはキーボード ショートカットまたはマウス クリック (携帯電話のタッチ) を通じてゲームを制御できるため、一方では、キー イベントをキャプチャしたり、クリックまたはタッチ イベントをバインドしたりするために、同じ機能を実行します。これらの論理式を$c.expグローバル変数に記述します。
キーコード ($event.keyCode) をアクション名に関連付けて、次のように入力します$v.KEYS

{
    "32": "drop",
    "37": "moveLeft",
    "38": "rotate",
    "39": "moveRight",
    "40": "down",
    "80": "pause",
    "82": "replay",
    "83": "sound"
}

キーが押されたとき (onKeyDown)、またはマウスが押されたとき (onMouseDown) に実行されます。

$v.activeKey = $event.keyCode
$c.exp[$v.KEYS[$v.activeKey]].exc()

activeKey は現在押されているキーを示し、$v.KEYS[$v.activeKey] は上で実行されるアクションの名前です。これらのアクションの定義は $c.exp に配置されており、以下で 1 つずつ説明します。以下のページをご覧ください。

キーが押されたとき (onKeyUp)、またはマウスが放されたとき (onMouseUp) に実行されます。

$v.activeKey = undefined
$(".key > .active").removeClass("active")

ゲーム開始:スタート

$v.score = 0
$v.status = "fall"
$v.startline ? array($v.startline).forEach($c.exp.startline) : ""
$c.exp.next.exc()

スコアは 0 に設定され、状態はフォールに設定され、スタート ラインが設定されている場合はスタート ラインを初期化して次のテトリスを生成します。

スタートラインを初期化します: startline

$v.matrix[19 - $index] = array(10,1)
array(1 + $w.Math.floor($w.Math.random() * 5)).forEach('$v.matrix[19 - $ext.$index][$w.Math.floor($w.Math.random() * 9)] = 0')

まず開始行の各行の小さな正方形を 1 に設定し、次にいくつかの正方形をランダムにくり抜きます (0 に設定)

次のテトリスを生成します: 次へ

$v.currB = $v.nextB
$v.nextB = $v.blocks[$w.Math.floor($w.Math.random() * $v.blocks.length)]
$v.xy = [3, -1]
$c.exp.fall.exc()

そこから$v.blocksランダムにテトリスブロックを生成し(currBは現在のブロックの現在のブロックを表し、nextBは次のブロックの次のブロックを表します)、その位置を先頭に設定します(xの開始点は3、中間位置、開始位置) y の点は -1、つまりまだ行列の外側にあり、見えません)。
動的クラス名を使用すると$v.currB[$parent.$index - $v.xy[1]][$index - $v.xy[0]]) ? " c" : ""、新しく生成されたブロックもマトリックス内にレンダリングされます。

落下の開始: 落下

stopIf($v.status !== "fall" || ($l.fall && $l.fall !== $v.fall))
$c.exp.meetIf.exc()
$v.xy[1] = $v.xy[1] + 1
render()
timeout((10 - $v.level) * ($v.activeKey ? 200 : 100))
$c.exp.fall.exc()

落下状態ではない場合、またはプレイヤーが下り坂操作を行っている場合には、落下動作を停止する。
まずは衝突を確認し、問題なければY軸に1つ追加してrender()するとテトリスの線が落ちているのが確認できます。
プレイヤーのレベル($v.level)に応じて一時停止時間(タイムアウト)が決定され、一時停止時間が短いほど落下速度が速くなり、再帰的に落下(自己)が実行されます。

衝突チェック:meetIf

stopIf($v.status != "fall" || !$v.currB.length)
stopIf($v.currB.some('$x.some("$x && $v.matrix[$ext.$index + $v.xy[1] + 1][$index + $v.xy[0]] !== 0")'), $c.exp.meet)

状態が落ちなくなった場合 (ゲームが終了したか、プレーヤーがリプレイを押した場合)、または衝突が発生した場合 ($v.currB = [])、チェックを続行しません。
テトリスの任意の小箱(つまり$x == 1)が1行下に移動した位置に小箱があるとすると、今度は落ちると衝突することになります。

衝突がありました: 衝突しました

stopIf($v.xy[1] === -1, $c.exp.replay)
$v.currB.forEach('$x.forEach("$x ? $c.exp.meet2.exc() : null")')
$v.currB = []
render()
timeout(300)
$$(".matrix .red").removeClass("red")
timeout(300)
$v.score = ($v.score || 0) + $v.level
$l.clearY = []
$v.matrix.forEach('$x.includes(0) ? "" : $l.clearY.push($index)')
$l.clearY.length ? $c.exp.clearline.exc() : timeout(100)
$c.exp.next.exc()

衝突位置の Y 軸が -1 の場合、ロシアン キューブが衝突したことを意味し、ゲームは終了し、リプレイ ロジックのリプレイが実行されます。
衝突アクションを実行する

衝突アクション:meet2

$v.matrix[$ext.$index + $v.xy[1]][$index + $v.xy[0]] = 1
$v.el.matrix.children[$ext.$index + $v.xy[1]].children[$index + $v.xy[0]].addClass("red")

テトリスの現在位置をフィールド マトリックスに入力し、これらの衝突する小さな正方形に赤いクラス名を追加して、その色を赤く強調表示します。

前の「衝突が発生しました」ロジックを実行し続け、$v.currB をクリアしてレンダリングし、300 マイクロ秒間一時停止し、衝突している小さな四角形の赤いハイライトを削除し、プレーヤーのレベルに応じてポイントを追加するためにさらに 300 マイクロ秒間一時停止します。プレイヤーのレベルが高いほど落下速度が速くなり、獲得できるポイントも高くなります。

どの行 (0 を含まない行) を削除できるかを確認します。行列の各行をループします。行の配列に 0 の小さな正方形がない場合、この行はすべて 1 であり、行であることを意味します。満たされており、削除することができます。その後、次のロジックを実行し続けて次のブロックを生成し、落下を続けます。

消行:clearline

$l.clearY.forEach('$v.el.matrix.children[$x].addClass("red")')
timeout(300)
$l.clearY.forEach('$v.el.matrix.children[$x].removeClass("red")')
timeout(310)
$l.clearY.forEach('$v.matrix.splice($x, 1); $v.matrix.splice(0, 0, array(10, 0))')
$v.score = ($v.score || 0) + $w.Math.pow(2, $l.clearY.length) * $v.level * 10
$v.lines = ($v.lines || 0) + $l.clearY.length

削除する行に赤いクラス名を付けると、300 マイクロ秒後にクラス名がキャンセルされ、視覚的に赤い点滅効果が表示されます。
マトリックスから保留行を削除し、マトリックスの先頭に空白行を追加します。
消すラインの数とプレイヤーのレベルに応じてスコアが増加し、一度に消すラインの数が多いほど、幾何レベル(2乗)が増加してスコアが高くなります。
削除された行の数を累積します。

落下プロセス中、プレイヤーはテトリスの動きを制御できます: 左に移動、右に移動、回転、落下、落下。

左に移動: moveLeft

$l.lr = -1
$c.exp.moveLR.exc()

右に移動: moveRight

$l.lr = 1
$c.exp.moveLR.exc()

移動ロジック: moveLR

stopIf(!$v.activeKey || $v.currB.some('$x.some("$x && $v.matrix[$ext.$index + $v.xy[1]][$index + $v.xy[0] + $l.lr] !== 0")'))
stopIf($v.status === "paused", '$v.status = "fall"; ' + $c.exp.fall)
$v.xy[0] = $v.xy[0] + $l.lr
render()
timeout(150 - $v.level * 10)
$c.exp.moveLR.exc()

左右に移動するときは境界チェックが必要で、左右の境界をずらすことはできません。現在のテトリスの任意の小さな正方形の X 軸上に 1 の小さな正方形があり、移動方向に 1 ステップ加えたものがある場合 (つまり $ l.lr) は、一線を越えるにはロジックの移動を停止することを意味します。
実際に左右に一歩移動してレンダリングし、しばらく待つと、プレイヤーのレベルが上がるほど待ち時間が少なくなり、つまり速度が速くなります。
ユーザーがまだボタンまたはマウスを押している場合 (つまり、最初の行の !$v.activeKey)、プレイヤーが手を離すか、境界を越えるか、衝突するまで、この移動ロジックを実行し続けます。

回転:回転

$l.currB = []
$v.currB.length === 2 ? $c.exp.rotate2to4.exc() :  $c.exp.rotate4to2.exc()
stopIf($l.currB.some('$x.some("$x && $v.matrix[$ext.$index + $l.xy[1]][$index + $l.xy[0]] !== 0")'))
$v.currB = $l.currB
$v.xy = $l.xy

現在のテトリスが水平 (2 行のみ) の場合は垂直 (rotate2to4)、そうでない場合は水平になります。
これは、回転プロセス中に回転できるかどうかをテストするために使用される一時変数であることに注意してください$l。最初に回転 (行と列を一時的に交換し、座標を変換して $l.currB に置きます) してから、位置 (一時的な位置を変更します) $l.xy

上げてください:rotate2to4

array(4).forEach('$l.currB.push([$v.currB[1][$index], $v.currB[0][$index]])')
$l.xy = [$v.xy[0] + 1, $v.xy[1] - 1]

リカンベント: 4 から 2 に回転

array(2).forEach('$l.currB.push([$v.currB[3][$index], $v.currB[2][$index], $v.currB[1][$index], $v.currB[0][$index]])')
$l.xy = [$v.xy[0] - 1, $v.xy[1] + 1]

テストが終了したら、衝突がないかを確認し、OK になったら、実際に回転を実行します。一時変数を$lに代入します$v

下り坂の開始: 下り

stopIf($v.status === "paused", '$v.status = "fall"; ' + $c.exp.fall)
$v.fall = date()
$l.fall = $v.fall
$c.exp.downLoop.exc()

現在一時停止状態にある場合は、落下状態に切り替えて落下動作を開始しますが、下り動作は行わないでください。
$v.fall と $l.fall の 2 つの変数による排他的実行、つまり下り坂時の自然な落下を一時停止します。

下り坂ロジック: downLoop

$c.exp.meetIf.exc()
$v.xy[1] = $v.xy[1] + 1
render()
timeout(20 - $v.level)
stopIf($v.activeKey, $c.exp.downLoop)
$c.exp.fall.exc()

衝突状況を確認し、問題がなければ1行下に進み、レンダリングし、プレイヤーのレベルに応じて非常に短い時間待ちます。プレイヤーが水を送らない場合は、急速に落下し続けます。プレイヤーが手を離すと自然に落下します。

ドロップの開始: ドロップ

stopIf($v.status === "over")
stopIf($v.status === "ready", $c.exp.start)
stopIf($v.status === "paused", '$v.status = "fall"; ' + $c.exp.fall)
$v.el.top.addClass("dropping")
$v.fall = date()
timeout(9)
$v.el.top.removeClass("dropping")
$c.exp.dropLoop.exc()

それが過ぎれば落ちません。
レディ状態でキーを押すとドロップせずにゲームが開始されます。
一時停止状態で押したキーが下降せず、下降し始めた場合。

ドロップロジック:dropLoop

$c.exp.meetIf.exc()
$v.xy[1] = $v.xy[1] + 1
$c.exp.dropLoop.exc()

フォールは懸垂下降と非常に似ており、底に着くか着底するまで連続して落ちますが、人間の目から見ると、フォールは1つのステップであり、中間のフォールプロセスはありません。つまり、フォール中に一時停止がないためです。レンダリング(線が落ちていても、レンダリングしなければ人間の目には見えません)。

テトリス ブロックの操作に加えて、プレイヤーはゲームを一時停止したり、効果音を切り替えたり、リプレイしたりすることもできます。

ゲームを一時停止する: 一時停止する

stopIf($v.status === "over")
stopIf($v.status === "ready", $c.exp.start)
stopIf($v.status === "fall", '$v.status = "paused"; ' + $c.exp.pauseAnim)
$v.status = "fall"
$c.exp.fall.exc()

終わったら一時停止する必要はありません。
準備完了状態でキーを押すと回転してゲームがスタートします。
落下状態でキーを押すと一時停止状態にしてアニメーションの一時停止を開始します。それ以外の場合は落下動作に変わります(一時停止状態で再度押す)。

アニメーションの一時停止:pauseAnim

toggleClass($v.el.paused, "c")
timeout(300)
$v.status === "paused" ? $c.exp.pauseAnim.exc() : ""

右側の一時停止アイコンを 300 マイクロ秒ごとに点滅するように変更します。

スイッチ効果音:音

$v.music = !$v.music

効果音をオンにすると、多くのアクションでサウンドが再生され、そのアクションの中に次のような表現があることがわかります。

$v.music && $audio.start(when, offset, duration)

リプレイ:リプレイ

stopIf($v.status === "ready", $c.exp.start)
$v.currB = []
$v.hiscore = $w.Math.max($v.hiscore, $v.score)
$v.lines = 0
$v.matrix = array(20, array(10))
$v.status = "over"
render()
array(20).forEach('$v.el.matrix.children[19 - $index].addClass("c"); timeout(30)')
timeout(100)
array(20).forEach('$v.el.matrix.children[$index].removeClass("c"); timeout(30)')
$v.status = "ready"
render()
$c.exp.readyAnim.exc()

準備完了状態でキーを押すと回転してゲームがスタートします。
現在のテトリスの構成要素をクリアし、最高スコアを計算し、累積行数をゼロにリセットし、マトリックスをクリアし、状態をオーバーに設定してレンダリングすると、この時点では空のフィールドであることがわかります。
次に、画面を消去するアニメーションを作成します。最初にすべての小さな正方形を下の行から 1 つずつ黒に変え、1 行が終了したら 30 マイクロ秒間停止します。すべての行が終了したら 100 マイクロ秒間停止し、上から黒を消去します。行がクリアされるたびに 30 マイクロ秒間一時停止します。

レンダリング後の準備完了状態の恐竜アニメーション

準備完了アニメーション:readyAnim

timeout(1000)
$(".readyscore").toggleClass("last")
$l.dragon = $(".dragon")
stopIf(!$l.dragon)
$l.dragonClass = "r"
array(40).forEach($c.exp.readyAnim2)
timeout(1000)
$l.dragonClass = "l"
array(40).forEach($c.exp.readyAnim2)
$v.status === "ready" ? $c.exp.readyAnim.exc() : ""

準備完了状態では、最高スコアと最終ラウンドのスコアが 1 秒ごとに交互に表示されます。
恐竜のクラス名を1秒ごとに右から左、左から右に回転させ、その間に恐竜のアニメーションを実行します。

恐竜アニメーション:readyAnim2

$l.dragon.className = "dragon " + $l.dragonClass + ($index + 1) % 4
timeout(60)

恐竜のクラス名を 60 マイクロ秒ごとに変更して、メガネと脚をアニメーション化します。

シーンの保存/復元: 可視性の変更

Web ページを閉じたり、更新したり、ウィンドウを切り替えたり、携帯電話のバッテリーが切れたり、着信が中断したりした場合は、次回ゲームに戻ったときに中断したところからプレイを継続できることを確認する必要があります。これらの状態は、実際には、visibilitychange イベント内の VisibilityState の非表示の状態です。非表示状態に切り替えると、すべての状態が localStorage に保存されます。

$w.document.addEventListener('visibilitychange', func('$w.document.visibilityState === "hidden" ? $exp.save.exc() : $exp.restore.exc()'))

保存ステータス: 保存

localStorage("tetris", {music: $v.music, startline: $v.startline, level: $v.level, score: $v.score, lines: $v.lines, currB: $v.currB, nextB: $v.nextB, status: $v.status === "over" ? "ready" : $v.status, hiscore: $w.Math.max($v.hiscore, $v.score), matrix: $v.status === "over" ? array(20, array(10)) : $v.matrix, xy: $v.xy[1] > 19 ? [3, -1] : $v.xy})
$v.status === "fall" ? $v.status = "paused" : ""

Hidden は状態を一時停止に切り替えます。

復元ステータス: 復元

$l.LS = localStorage("tetris")
$v.score = $l.LS.score || 0
...
render()
$v.status === "fall" ? exc('timeout(2000); ' + $c.exp.fall) : ""

localStorageから状態データを読み取った後、変数に部分的に値を代入します。
ポイントは、セーブ前に落下状態になった場合は、2秒待ってユーザーの反応を待ってから落下を続けることです。

この時点で、ゲーム ロジック全体が完了しました。次の質問でプロセス全体を確認できます。

  • キーボード入力を取得するにはどうすればよいですか?

  • ブロックの動きを制御するにはどうすればよいですか?

  • ゲーム内のさまざまな形状やゲーム空間全体をどのようにデータで表現できるのでしょうか?

  • ゲーム内で左右や下に移動する可能性をどのように判断するか?

  • ゲーム内で特定の形状の回転の可能性を判断するにはどうすればよいですか?

  • 下矢印キーを押したときに図形の落下速度を加速するにはどうすればよいですか?

  • ある形が終わりに達したことをどのように判断するのでしょうか?

  • 行が埋まったことをどのように判断するのでしょうか?

  • 特定の図形が一番下に落ちた後に削除できる行をすべて削除するにはどうすればよいですか?

  • ゲームボードの状態を変更するにはどうすればよいですか?

  • スコアの数え方は?

  • アップグレード後の加速の問題にどう対処すればよいですか?

  • ゲームの終わりをどう判断するか?

深く学習する準備ができている学生は、テトリスのページにアクセスし、右側の [クローン] ボタンをクリックして、ゲーム全体のコピーを作成し、プレイしたり、自由に変更したりできます。

さらに多くの教育ビデオについては、Bilibili スペースにアクセスしてください。Zhongtouchアプリケーション プラットフォームのパーソナル スペース_哔哩哔哩_Bilibili には、さまざまなフロントエンド視覚化ケースのデモンストレーションと説明だけでなく、複数の完全に機能する Web サイト アプリケーション ケースもあります。開発プロセスのデモンストレーションと説明。

この事件は2021年6月25日にテトリスを模倣したものであり、関連資料の著作権はオリジナルのウェブサイトに属します。このコースは教育のみを目的としています。

おすすめ

転載: blog.csdn.net/weixin_52095264/article/details/125826839