🔴

ぷよぷよプログラミングをGodotで実装 02 コード理解(stage.js)

2025/01/27に公開

はじめに

前回に引き続きぷよぷよプログラミングを利用して、Godotでパズルゲームの作り方の基礎を一緒に学んでいこうと思います。YouTubeでもこの記事の内容に沿ってゲームを作っているので、動画を見ながら取り組んでみてください。

https://youtu.be/xWCfGvPEpp4

前回の記事はこちら
https://zenn.dev/yurinchi/articles/77241679201f44

利用教材

この記事で学ぶこと

godotを触る前にまずはぷよぷよプログラミングのソースコード(javascript)について解説をしていきます。
今回は主にstage.jsについて解説します。

stage.js の概要

このクラスでは、ぷよの配置や移動、消去のロジックが実装されています。ゲームの進行を管理し、プレイヤーがぷよを積み上げて消していく過程を処理します。

stage.js の構成

プロパティ

stageElement

ゲームステージのHTML要素を保持します。ゲームのプレイエリアを設定し、ぷよの配置や背景色などを設定します。

scoreElement

スコアを表示するHTML要素を保持します。ステージの下部にスコアを表示し、その背景色などを設定します。

zenkeshiImage

全消し(フィールド上の全てのぷよを消す)を表示するための画像要素を保持します。全消しが発生した際に表示し、アニメーションを行います。

board

ゲームのフィールド情報を保持する2次元配列です。ぷよの配置や状態を管理します。フィールド上の各セルがぷよの情報を持ちます。

puyoCount

現在フィールド上に存在するぷよの数を保持します。ゲームの進行や得点計算に使用されます。

fallingPuyoList

自由落下中のぷよの情報をリストとして保持します。ぷよが自由落下しているかどうかをチェックし、位置を更新するために使用します。

eraseStartFrame

ぷよを消すアニメーションの開始フレームを保持します。アニメーションの進行状況を計算するために使用します。

erasingPuyoInfoList

消去対象のぷよの情報をリストとして保持します。ぷよを消す際にその情報を使用してアニメーションを実行します。

メソッド

initialize

ゲームステージの初期化を行います。HTML要素を取得し、ステージ、スコア、全消し画像の設定、メモリの準備などを行います。
ゲームの初期設定をまとめて行うため、ステージのサイズや背景色、初期配置など一度に設定できます。

主にHTMLに合わせた内容となっています。

下記の内容はHTMLのstageという要素を見つけ出し、盤面として利用するため、横幅と縦幅を決めていいます。横幅と縦幅はコンフィグファイルで設定した画像の幅と配置する工数から算出しています。

  const stageElement = document.getElementById("stage");
  stageElement.style.width = Config.puyoImgWidth * Config.stageCols + 'px';
  stageElement.style.height = Config.puyoImgHeight * Config.stageRows + 'px';
  stageElement.style.backgroundColor = Config.stageBackgroundColor;
  this.stageElement = stageElement;

下記の内容はHTMLのzenkeshiという要素を見つけ出し、全消しした時に表示させる画像のサイズを設定しています。横は幅はぷよの横幅6個分として、非表示の状態で要素を追加しています。

  const zenkeshiImage = document.getElementById("zenkeshi");
  zenkeshiImage.width = Config.puyoImgWidth * 6;
  zenkeshiImage.style.position = 'absolute';
  zenkeshiImage.style.display = 'none';        
  this.zenkeshiImage = zenkeshiImage;
  stageElement.appendChild(zenkeshiImage);

配置状態を管理するため、盤面同じ横6マス、縦12マスの多次元配列を用意します。
ぷよがない場合は、0、ぷよが存在する場合は種類(色)に合わせて1以上の値が設定されます。

  // メモリを準備する
  this.board = [
      [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, 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],
  ];

おそらくプログラミングを理解しやすくするため、直接配列をしていると思います。
Godot化する時は折角コンフィグファイルに幅と高さの情報を持っているので、そちらを利用するようにしたいですね。

下記は盤面のメモリに合わせて画面表示にも反映しています。また同時にぷよの数をカウントしています。
初期化時なので必ず0になると思いますが、今後拡張して初期にお邪魔ぷよを配置したりする場合には意味をなしてきますね。

let puyoCount = 0;
for(let y = 0; y < Config.stageRows; y++) {
    const line = this.board[y] || (this.board[y] = []);
    for(let x = 0; x < Config.stageCols; x++) {
        const puyo = line[x];
        if(puyo >= 1 && puyo <= 5) {
            // line[x] = {puyo: puyo, element: this.setPuyo(x, y, puyo)};
            this.setPuyo(x, y, puyo);
            puyoCount++;
        } else {
            line[x] = null;
        }
    }
}
this.puyoCount = puyoCount;
  • let puyoCount = 0;
    puyoCount という変数を初期化しています。この変数はフィールド上のぷよの総数をカウントするためのものです。

  • for(let y = 0; y < Config.stageRows; y++) { ... }
    盤面の行(Y軸)をループしています。Config.stageRows は盤面の高さ(行数)を示します。

  • const line = this.board[y] || (this.board[y] = []);
    this.board[y] が存在しない場合は、新しく空の配列を作成して this.board[y] に代入します。この行に対応するぷよの情報を格納する配列です。

  • for(let x = 0; x < Config.stageCols; x++) { ... }
    盤面の列(X軸)をループしています。Config.stageCols は盤面の幅(列数)を示します。

  • const puyo = line[x];
    現在のセルにあるぷよの情報を puyo 変数に格納します。ぷよの値は色や種類を表しています。

  • if(puyo >= 1 && puyo <= 5) { ... }
    puyo が 1 から 5 の範囲内である場合(つまり、ぷよが存在する場合)に処理を行います。ぷよがない場合は次に進みます。

  • this.setPuyo(x, y, puyo);
    setPuyo メソッドを呼び出し、指定された位置(X, Y)にぷよを設定します。このメソッドは画面にぷよを表示し、メモリ上にも情報を設定します。

  • puyoCount++;
    puyoCount を 1 増やします。これにより、盤面上のぷよの数をカウントします。

  • } else { line[x] = null; }
    puyo が存在しない場合は line[x] に null を設定します。このセルが空であることを示します。

  • this.puyoCount = puyoCount;
    最後に、カウントしたぷよの総数を this.puyoCount に設定します。この変数はゲーム全体でぷよの総数を管理するために使用されます。

setPuyo

引数 説明
x 配置するぷよの X 座標(列番号)
y 配置するぷよの Y 座標(行番号)
puyo 配置するぷよの種類や色を示す整数値

指定された位置(x, y)にぷよを配置し、画面とメモリ両方にその情報をセットします。
画面にぷよを表示するだけでなく、メモリ上でも配置情報を保持することで、後の処理(落下や消去)を簡単に行えます。

// 画面とメモリ両方に puyo をセットする
static setPuyo(x, y, puyo) {
    // 画像を作成し配置する
    const puyoImage = PuyoImage.getPuyo(puyo);
    puyoImage.style.left = x * Config.puyoImgWidth + "px";
    puyoImage.style.top = y * Config.puyoImgHeight + "px";
    this.stageElement.appendChild(puyoImage);
    // メモリにセットする
    this.board[y][x] = {
        puyo: puyo,
        element: puyoImage
    }
}
画像を作成し配置する
  • const puyoImage = PuyoImage.getPuyo(puyo);
    PuyoImage.getPuyo(puyo) を呼び出して、指定された種類のぷよの画像要素を取得します。

  • puyoImage.style.left = x * Config.puyoImgWidth + "px";
    ぷよの画像の左位置を設定します。x 座標に基づいて計算され、1つのぷよの幅 (Config.puyoImgWidth) によって配置されます。

  • puyoImage.style.top = y * Config.puyoImgHeight + "px";
    ぷよの画像の上位置を設定します。y 座標に基づいて計算され、1つのぷよの高さ (Config.puyoImgHeight) によって配置されます。

  • this.stageElement.appendChild(puyoImage);
    取得したぷよの画像要素を stageElement に追加します。これにより、画面上にぷよが表示されます。

メモリにセットする
  • this.board[y][x] = { puyo: puyo, element: puyoImage }
    メモリ上の board 配列にぷよの情報を設定します。具体的には、指定された位置 (x, y) にぷよの種類(色)と画像要素を保持するオブジェクトを格納します。

この setPuyo メソッドはゲームの視覚的な構成要素とデータ構造を連携させる中心的な役割を担っています。

checkFall

戻り値 説明
bool 少なくとも1つのぷよが落下する場合は true を返し、それ以外は false を返します

自由落下が可能かどうかをチェックします。ぷよが落ちる場合、その情報を fallingPuyoList に追加します。
フィールドの下から上に向かってチェックすることで、効率的に自由落下を判断します。

// 自由落下をチェックする
static checkFall() {
    this.fallingPuyoList.length = 0;
    let isFalling = false;
    // 下の行から上の行を見ていく
    for(let y = Config.stageRows - 2; y >= 0; y--) { 
        const line = this.board[y];
        for(let x = 0; x < line.length; x++) {
            if(!this.board[y][x]) {
                // このマスにぷよがなければ次
                continue;
            }
            if(!this.board[y + 1][x]) {
                // このぷよは落ちるので、取り除く
                let cell = this.board[y][x];
                this.board[y][x] = null;
                let dst = y;
                while(dst + 1 < Config.stageRows && this.board[dst + 1][x] == null) {
                    dst++;
                }
                // 最終目的地に置く
                this.board[dst][x] = cell;
                // 落ちるリストに入れる
                this.fallingPuyoList.push({
                    element: cell.element,
                    position: y * Config.puyoImgHeight,
                    destination: dst * Config.puyoImgHeight,
                    falling: true
                });
                // 落ちるものがあったことを記録しておく
                isFalling = true;
            }
        }
    }
    return isFalling;
}
プロパティのリセット
  • this.fallingPuyoList.length = 0;
    自由落下中のぷよのリストをリセットします。これにより、新たにチェックされるぷよの情報を保持する準備をします。

  • let isFalling = false;
    isFalling というフラグを初期化します。このフラグは、ぷよが少なくとも1つでも落下するかどうかを示します。

行のループ
  • for(let y = Config.stageRows - 2; y >= 0; y--) { ... }
    下の行から上の行に向かってループを実行します。Config.stageRows - 2 は、最下段の1つ上の行から開始するためのものです。これは、最下段は落下対象ではないためです。
列のループ
  • for(let x = 0; x < line.length; x++) { ... }
    各行の各列をループします。ぷよの配置をチェックします。
ぷよの存在チェック
  • if(!this.board[y][x]) { ... }
    現在のセルにぷよがない場合は次のセルに進みます。
落下チェック
  • if(!this.board[y + 1][x]) { ... }
    現在のぷよの下のセルに何もない場合、ぷよは落下するので、そのぷよを取り除きます。
ぷよの移動
  • let cell = this.board[y][x];
    現在のぷよを cell に一時保存します。

  • this.board[y][x] = null;
    現在のセルを空にします。

  • let dst = y;
    目的地の初期値を現在の行に設定します。

  • while(dst + 1 < Config.stageRows && this.board[dst + 1][x] == null) { ... }
    下のセルが空であり続ける限り、ぷよは下に落下し続けます。最終的に目的地のセルに到達します。

全体的に階層が深いのでGodotにする際はもう少し階層が深くならないように書き換えようと思います。

fall

fallingPuyoList に基づいてぷよを自由落下させます。ぷよの位置を更新し、落下が続いているかどうかを返します。
落下のスピードをConfig.freeFallingSpeedで設定できるため、ゲームの動作を調整しやすいです。

// 自由落下させる
static fall() {
    let isFalling = false;
    for(const fallingPuyo of this.fallingPuyoList) {
        if(!fallingPuyo.falling) {
            // すでに自由落下が終わっている
            continue;
        }
        let position = fallingPuyo.position;
        position += Config.freeFallingSpeed;
        if(position >= fallingPuyo.destination) {
            // 自由落下終了
            position = fallingPuyo.destination;
            fallingPuyo.falling = false;
        } else {
            // まだ落下しているぷよがあることを記録する
            isFalling = true;
        }
        // 新しい位置を保存する
        fallingPuyo.position = position;
        // ぷよを動かす
        fallingPuyo.element.style.top = position + 'px';
    }
    return isFalling;
}
落下フラグの初期化
  • let isFalling = false;
    isFalling というフラグを初期化します。このフラグは、少なくとも1つのぷよが落下中であるかどうかを示します。
落下中のぷよのループ
  • for(const fallingPuyo of this.fallingPuyoList) { ... }
    fallingPuyoList の中の各ぷよについてループを実行します。
落下終了のチェック
  • if(!fallingPuyo.falling) { ... }
    現在のぷよが既に落下を終えている場合は次のぷよに進みます。
位置の更新
  • let position = fallingPuyo.position;
    現在のぷよの位置を取得します。

  • position += Config.freeFallingSpeed;
    ぷよの位置を自由落下の速度 Config.freeFallingSpeed に基づいて更新します。

落下終了の判定
  • if(position >= fallingPuyo.destination) { ... }
    ぷよが目的地に到達したかどうかをチェックします。

  • position = fallingPuyo.destination;
    ぷよの位置を最終目的地に設定します。

  • fallingPuyo.falling = false;
    ぷよの落下が終了したことを記録します。

落下中のぷよの記録
  • else { isFalling = true; }
    ぷよがまだ落下中であることを記録します。これにより isFalling フラグが true になります。
新しい位置の保存
  • fallingPuyo.position = position;
    更新されたぷよの位置を保存します。
ぷよの移動
  • fallingPuyo.element.style.top = position + 'px';
    ぷよの画像要素を新しい位置に移動させます。これにより、画面上でぷよが実際に落下しているように見えます。
落下状況の返却
  • return isFalling;
    少なくとも1つのぷよがまだ落下中であるかどうかのフラグを返します。

ぷよの位置を更新する際に、自由落下の速度 Config.freeFallingSpeed を使っているため、ぷよが滑らかに落下します。これにより、落下のアニメーションが自然に見えます。
ぷよが目的地に到達したかどうかをチェックし、到達した場合は正確な位置に配置します。これにより、ぷよが正確に落下していることを保証します。

checkErase

引数 説明
startFrame 開始フレーム

指定されたフレームから連続して並んだぷよが消せるかどうかを判定します。消せる場合、その情報を erasingPuyoInfoListに追加します。
連続しているぷよの判定を再帰的に行うことで、隣接するぷよを効率的にチェックできます。

// 消せるかどうか判定する
static checkErase(startFrame) {
    this.eraseStartFrame = startFrame;
    this.erasingPuyoInfoList.length = 0;

    // 何色のぷよを消したかを記録する
    const erasedPuyoColor = {};

    // 隣接ぷよを確認する関数内関数を作成
    const sequencePuyoInfoList = [];
    const existingPuyoInfoList = [];
    const checkSequentialPuyo = (x, y) => {
        // ぷよがあるか確認する
        const orig = this.board[y][x];
        if(!orig) {
            // ないなら何もしない
            return;
        }
        // あるなら一旦退避して、メモリ上から消す
        const puyo = this.board[y][x].puyo;
        sequencePuyoInfoList.push({
            x: x,
            y: y,
            cell: this.board[y][x]
        });
        this.board[y][x] = null;

        // 四方向の周囲ぷよを確認する
        const direction = [[0, 1], [1, 0], [0, -1], [-1, 0]];
        for(let i = 0; i < direction.length; i++) {
            const dx = x + direction[i][0];
            const dy = y + direction[i][1];
            if(dx < 0 || dy < 0 || dx >= Config.stageCols || dy >= Config.stageRows) {
                // ステージの外にはみ出た
                continue;
            }
            const cell = this.board[dy][dx];
            if(!cell || cell.puyo !== puyo) {
                // ぷよの色が違う
                continue;
            }
            // そのぷよのまわりのぷよも消せるか確認する
           checkSequentialPuyo(dx, dy); 
            
        };
    };
    
    // 実際に削除できるかの確認を行う
    for(let y = 0; y < Config.stageRows; y++) {
        for(let x = 0; x < Config.stageCols; x++) {
            sequencePuyoInfoList.length = 0;
            const puyoColor = this.board[y][x] && this.board[y][x].puyo;
            checkSequentialPuyo(x, y);
            if(sequencePuyoInfoList.length == 0 || sequencePuyoInfoList.length < Config.erasePuyoCount) {
                // 連続して並んでいる数が足りなかったので消さない
                if(sequencePuyoInfoList.length) {
                    // 退避していたぷよを消さないリストに追加する
                    existingPuyoInfoList.push(...sequencePuyoInfoList);
                }
            } else {
                // これらは消して良いので消すリストに追加する
                this.erasingPuyoInfoList.push(...sequencePuyoInfoList);
                erasedPuyoColor[puyoColor] = true;
            }
        }
    }
    this.puyoCount -= this.erasingPuyoInfoList.length;

    // 消さないリストに入っていたぷよをメモリに復帰させる
    for(const info of existingPuyoInfoList) {
        this.board[info.y][info.x] = info.cell;
    }

    if(this.erasingPuyoInfoList.length) {
        // もし消せるならば、消えるぷよの個数と色の情報をまとめて返す
        return {
            piece: this.erasingPuyoInfoList.length,
            color: Object.keys(erasedPuyoColor).length
        };
    }
    return null;
}
消去したぷよの色を記録するオブジェクト
  • const erasedPuyoColor = {};
    何色のぷよを消したかを記録するためのオブジェクトを初期化します。
隣接するぷよを確認する関数
  • const sequencePuyoInfoList = [];
    連続しているぷよの情報を一時的に保持するリストです。

  • const existingPuyoInfoList = [];
    消さないぷよの情報を保持するリストです。

  • 関数 checkSequentialPuyo(x, y)
    指定された位置から隣接するぷよを再帰的にチェックします。

const checkSequentialPuyo = (x, y) => {
    const orig = this.board[y][x];
    if(!orig) {
        return;
    }
    const puyo = this.board[y][x].puyo;
    sequencePuyoInfoList.push({ x: x, y: y, cell: this.board[y][x] });
    this.board[y][x] = null;

    const direction = [[0, 1], [1, 0], [0, -1], [-1, 0]];
    for(let i = 0; i < direction.length; i++) {
        const dx = x + direction[i][0];
        const dy = y + direction[i][1];
        if(dx < 0 || dy < 0 || dx >= Config.stageCols || dy >= Config.stageRows) {
            continue;
        }
        const cell = this.board[dy][dx];
        if(!cell || cell.puyo !== puyo) {
            continue;
        }
        checkSequentialPuyo(dx, dy); 
    }
};
消去対象のぷよをチェックする

フィールド全体をループし、各セルで checkSequentialPuyo(x, y) を呼び出して連続しているぷよをチェックします。

for(let y = 0; y < Config.stageRows; y++) {
    for(let x = 0; x < Config.stageCols; x++) {
        sequencePuyoInfoList.length = 0;
        const puyoColor = this.board[y][x] && this.board[y][x].puyo;
        checkSequentialPuyo(x, y);
        if(sequencePuyoInfoList.length == 0 || sequencePuyoInfoList.length < Config.erasePuyoCount) {
            if(sequencePuyoInfoList.length) {
                existingPuyoInfoList.push(...sequencePuyoInfoList);
            }
        } else {
            this.erasingPuyoInfoList.push(...sequencePuyoInfoList);
            erasedPuyoColor[puyoColor] = true;
        }
    }
}
消去対象のぷよ数をカウントする
  • this.puyoCount -= this.erasingPuyoInfoList.length;
    消去されるぷよの数をカウントし、フィールド上のぷよの総数から減算します。
消さないぷよをメモリに復帰させる

退避していた消さないぷよの情報を this.board に戻します。

for(const info of existingPuyoInfoList) {
    this.board[info.y][info.x] = info.cell;
}
消去可能なぷよの情報を返す

消去対象のぷよが存在する場合、その情報(消えるぷよの個数と色の数)を返します。消去対象がない場合は null を返します。

if(this.erasingPuyoInfoList.length) {
    return {
        piece: this.erasingPuyoInfoList.length,
        color: Object.keys(erasedPuyoColor).length
    };
}
return null;

erasing

引数 説明
frame Text

ぷよを消すアニメーションを実行します。アニメーションが終了したかどうかを返します。
消えるアニメーションをフレームに基づいて制御するため、滑らかなアニメーションが実現できます。

// 消すアニメーションをする
static erasing(frame) {
    const elapsedFrame = frame - this.eraseStartFrame;
    const ratio = elapsedFrame / Config.eraseAnimationDuration;
    if(ratio > 1) {
        // アニメーションを終了する
        for(const info of this.erasingPuyoInfoList) {
            var element = info.cell.element;
            this.stageElement.removeChild(element);
        }
        return false;
    } else if(ratio > 0.75) {
        for(const info of this.erasingPuyoInfoList) {
            var element = info.cell.element;
            element.style.display = 'block';
        }
        return true;
    } else if(ratio > 0.50) {
        for(const info of this.erasingPuyoInfoList) {
            var element = info.cell.element;
            element.style.display = 'none';
        }
        return true;
    } else if(ratio > 0.25) {
        for(const info of this.erasingPuyoInfoList) {
            var element = info.cell.element;
            element.style.display = 'block';
        }
        return true;
    } else {
        for(const info of this.erasingPuyoInfoList) {
            var element = info.cell.element;
            element.style.display = 'none';
        }
        return true;
    }
}
経過フレームの計算
  • const elapsedFrame = frame - this.eraseStartFrame;
    アニメーションの開始フレームからの経過フレーム数を計算します。
アニメーション比率の計算
  • const ratio = elapsedFrame / Config.eraseAnimationDuration;
    アニメーションの全体時間に対する現在の進行度を比率で計算します。
アニメーションの終了判定
  • if(ratio > 1) { ... }
    アニメーションが終了した場合の処理です。
for(const info of this.erasingPuyoInfoList) {
    var element = info.cell.element;
    this.stageElement.removeChild(element);
}
return false;

this.erasingPuyoInfoList に含まれる全てのぷよの要素をステージから削除し、アニメーション終了を示すために false を返します。

アニメーションの途中段階

アニメーションの進行度に応じてぷよの表示・非表示を切り替えます。

  • 進行度75%を超過
else if(ratio > 0.75) {
    for(const info of this.erasingPuyoInfoList) {
        var element = info.cell.element;
        element.style.display = 'block';
    }
    return true;
}

ぷよを表示します。

  • 進行度50%を超過75%以下
else if(ratio > 0.50) {
    for(const info of this.erasingPuyoInfoList) {
        var element = info.cell.element;
        element.style.display = 'none';
    }
    return true;
}

ぷよを非表示にします。

  • 進行度25%を超過50%以下
else if(ratio > 0.25) {
    for(const info of this.erasingPuyoInfoList) {
        var element = info.cell.element;
        element.style.display = 'block';
    }
    return true;
}

ぷよを再表示します。

  • 進行度25%以下
else {
    for(const info of this.erasingPuyoInfoList) {
        var element = info.cell.element;
        element.style.display = 'none';
    }
    return true;
}

ぷよを非表示にします。

アニメーションの進行状況を返す

アニメーションがまだ進行中であることを示すために true を返します。

アニメーションの進行度を比率で計算し、その進行度に応じて表示・非表示を切り替えることで、滑らかなアニメーションを実現します。
フレーム単位でアニメーションを制御することにより、タイミングを細かく調整し、視覚的な効果を最大限に引き出します。
Godotで実装する際にはGodotのスプライトアニメーションを利用するかなど実装方法の単純化もできそうですね。

showZenkeshi

全消し(フィールド上のぷよを全て消すこと)が発生した際にを表示するアニメーションを行います。
アニメーションを計算して実行するため、特別な演出を追加できます。

static showZenkeshi() {
    // 全消しを表示する
    this.zenkeshiImage.style.display = 'block';
    this.zenkeshiImage.style.opacity = '1';
    const startTime = Date.now();
    const startTop = Config.puyoImgHeight * Config.stageRows;
    const endTop = Config.puyoImgHeight * Config.stageRows / 3;
    const animation = () => {
        const ratio = Math.min((Date.now() - startTime) / Config.zenkeshiDuration, 1);
        this.zenkeshiImage.style.top = (endTop - startTop) * ratio + startTop + 'px';
        if(ratio !== 1) {
            requestAnimationFrame(animation);
        }
    };
    animation();
}
全消しを表示する
  • this.zenkeshiImage.style.display = 'block';
    全消しの画像要素 zenkeshiImage を表示します。display プロパティを block に設定することで、画像を画面上に表示します。

  • this.zenkeshiImage.style.opacity = '1';
    画像の不透明度を 1 に設定します。これにより、画像が完全に表示されるようになります。

アニメーションの開始時間と位置の設定
  • const startTime = Date.now();
    現在の時刻を取得し、アニメーションの開始時間として保存します。

  • const startTop = Config.puyoImgHeight * Config.stageRows;
    アニメーションの開始位置(縦方向)を設定します。これはステージの高さ(行数)に基づいて計算されます。

  • const endTop = Config.puyoImgHeight * Config.stageRows / 3;
    アニメーションの終了位置(縦方向)を設定します。ステージの高さの1/3の位置に設定されます。

アニメーションの関数

アニメーション比率の計算

const animation = () => {
    const ratio = Math.min((Date.now() - startTime) / Config.zenkeshiDuration, 1);
    this.zenkeshiImage.style.top = (endTop - startTop) * ratio + startTop + 'px';
    if(ratio !== 1) {
        requestAnimationFrame(animation);
    }
};
  • ratio は、現在の時刻と開始時刻の差を全体のアニメーション時間 Config.zenkeshiDuration で割ったものです。Math.min を使って比率が 1 を超えないようにしています。
  • this.zenkeshiImage.style.top は、開始位置から終了位置までの位置を比率に基づいて計算し、新しい位置を設定します。
アニメーションの開始
  • animation();
    アニメーション関数 animation を呼び出して、アニメーションを開始します。

hideZenkeshi

全消し表示のフェードアウトアニメーションを行い、アニメーションが終了したら画像を非表示にするためのものです。
アニメーションの終わりを見計らって表示を非表示にすることで、演出を自然に終えることができます。

static hideZenkeshi() {
    // 全消しを消去する
    const startTime = Date.now();
    const animation = () => {
        const ratio = Math.min((Date.now() - startTime) / Config.zenkeshiDuration, 1);
        this.zenkeshiImage.style.opacity = String(1 - ratio);
        if(ratio !== 1) {
            requestAnimationFrame(animation);
        } else {
            this.zenkeshiImage.style.display = 'none';
        }
    };
    animation();
}
アニメーションの開始時間を設定
  • const startTime = Date.now();
    現在の時刻を取得し、アニメーションの開始時間として保存します。
アニメーション関数の定義

アニメーション比率の計算

const animation = () => {
    const ratio = Math.min((Date.now() - startTime) / Config.zenkeshiDuration, 1);
    this.zenkeshiImage.style.opacity = String(1 - ratio);
    if(ratio !== 1) {
        requestAnimationFrame(animation);
    } else {
        this.zenkeshiImage.style.display = 'none';
    }
};
  • ratio は、現在の時刻と開始時刻の差を全体のアニメーション時間 Config.zenkeshiDuration で割ったものです。Math.min を使って比率が 1 を超えないようにしています。

  • this.zenkeshiImage.style.opacity = String(1 - ratio);
    画像の不透明度を 1 から 0 に変化させます。ratio が増加するにつれて、画像が徐々にフェードアウトしていきます。

アニメーションの継続
if(ratio !== 1) {
    requestAnimationFrame(animation);
} else {
    this.zenkeshiImage.style.display = 'none';
}
  • requestAnimationFrame(animation) を使って次のフレームでアニメーションを継続させます。これにより、ブラウザのリフレッシュレートに同期したスムーズなアニメーションを実現します。
  • ratio が 1 に達した場合(アニメーションが終了した場合)、画像を非表示にするために this.zenkeshiImage.style.display = 'none'; を設定します。
アニメーションの開始
  • animation();
    アニメーション関数 animation を呼び出して、フェードアウトアニメーションを開始します。

テクニックとポイント

再帰関数の利用

checkErase()メソッドで隣接するぷよをチェックする際に再帰関数を使っている点は、コードのシンプルさと効率性を両立させています。

アニメーション制御

フレームに基づくアニメーション制御を採用しているため、滑らかでタイムリーな演出が可能です。特にshowZenkeshi()hideZenkeshi()では、アニメーションの開始時間と現在時間を使ってアニメーションを計算しており、非常に動きのある演出が行われています。

落下処理の効率化

checkFall()では下の行から上の行をチェックすることで、効率よく自由落下を判定しています。これにより、必要な処理を減らしつつ、正確な落下判定が行えます。

まとめ

このコードにはゲームのロジックが詰まっていて、とても興味深いですね。
アニメーションの制御などjavascriptで実装している箇所に関してはなるべくGodotの機能を利用して実装していきたいと思います。
次の記事も引き続きJavaScriptの分析を続けていこうと思います。

Discussion