シェルスクリプトマガジン

香川大学SLPからお届け!(Vol.74掲載)

著者:増田 嶺

 前回に引き続き、SLPで開発したリズムゲームを紹介します。前回はゲームで使用する譜面作成ツールについて解説しました。今回は、ゲーム本体について解説します。ゲーム本体は「Visual Studio Code」(VSCode)で開発します。

図5 「js/game/singleNote.js」ファイルに記述するコード

class SingleNote {
  constructor(speed, reachTime) {
    this.height = note.height;
    this.width = note.width;
    this.frameColor = 'rgb(' + note.frameColor + ')'; // 枠の色
    this.bodyColor = 'rgba(' + note.bodyColor + ')';  // 中の色
    this.this.centerY = -this.height;                 // ノーツ中心のY座標
    this.speed = speed * note.speedRatio;             // px/ms
    // ゲーム開始から判定ラインに到達するまでの時間
    this.reachTime = reachTime + note.delay; 
    // canvasに入る時間
    this.appearTime = this.reachTime -
                      (JUDGE_LINE.centerY + this.height / 2) / this.speed;           
    // canvasから出る時間
    this.hideTime = this.reachTime + 
                    (CANVAS_H - JUDGE_LINE.centerY + this.height / 2) / 
                    this.speed;
    this.act = true;   // 自身がヒット処理対象かどうか
    this.show = false; // 自身がcanvasに描画されるかどうか
  }
}

図6 「js/game/backLane.js」ファイルに追加するコード

(略)
  generateNote(data) {
    this.note = data.map((val) => new SingleNote(val[2], val[3]));
  }
(略)

図7 「js/game/singleNote.js」ファイルに追加するコード

class SingleNote {
  constructor(speed, reachTime) {
(略)
  }
  draw(x) {
    if (this.show) {
      this.centerY = JUDGE_LINE.centerY - 
                     (this.reachTime - time.elapsed) * this.speed;
      CTX.fillStyle = this.bodyColor;
      CTX.fillRect(x, this.centerY - this.height / 2, 
                   this.width, this.height);
      CTX.strokeStyle = this.frameColor;
      CTX.strokeRect(
        x + LINE_WIDTH / 2,
        this.centerY - this.height / 2 + LINE_WIDTH / 2,
        this.width - LINE_WIDTH,
        this.height - LINE_WIDTH
      );
    }
  }
}

図8 「js/game/backLane.js」ファイルに追加するコード

(略)
  drawNote() {
    this.note.forEach(val => val.draw(this.x));
  }
(略)

図9 「js/game/singleNote.js」ファイルに追加するコード

class SingleNote {
  constructor(speed, reachTime) {
(略)
  }
  draw(x) {
(略)
  }
  update() {
    this.updateShow();
  }
  updateShow() {
    if (this.act || this.show) {
      if (this.appearTime <= time.elapsed && 
          time.elapsed <= this.hideTime) {
        this.show = true;
      } else {
        this.show = false;
      }
    }
  }
}

図11 「js/game/backLane.js」ファイルに追加するコード

(略)
  update() {
    this.note.forEach(val => val.update());
  }
(略)

図13 「js/game/backLane.js」ファイルに追加するコード

(略)
  judge() {
    calcElapsedTime(); // 経過時間を更新
    // ヒットしているノーツを抽出
    const TARGET = this.note.filter(val => val.checkHit(note.hitRange[3])); 
    // TARGETの先頭から処理、先頭ノーツのグレードがBadだった場合のみ
    // 二つ目以降のノーツを処理し、それらのノーツがBadだった場合は中断
    const GRADE = [3]
    for (let i = 0; TARGET[i] && GRADE[0] === 3; i++) {
      GRADE[i] = TARGET[i].getGrade();
      // 二つ目以降のノーツがBadの場合はそこで中断
      if (i > 0 && GRADE[i] === 3) {  
        break;
      }
      JUDGE_LINE.setGrade(GRADE[i]); // ノーツヒットのグレードを描画
      TARGET[i].close();             // 判定済みのノーツ処理を停止
    }
  }
(略)

図15 「js/game/singleNote.js」ファイルに追加するコード

class SingleNote {
(略)
  updateShow() {
(略)
  }
  checkHit(range) {
    if (this.act && 
        Math.abs(time.elapsed - this.reachTime) <= range) {
      return true;
    } else {
      return false;
    }
  }
  getGrade() {
    let grade = 3;
    for (let i = 2; i >= 0; i--) {
      if (this.checkHit(note.hitRange[i])) {
        grade = i;
      }
    }
    return grade;
  }
}

図16 「js/game/singleNote.js」ファイルに追加するコード

class SingleNote {
(略)
  update() {
    this.updateShow();
    if (!this.act) {
      return;
    }
    // 判定ラインから自身の判定ゾーンが
    // 過ぎた時点で処理
    if (this.reachTime < time.elapsed &&
        !this.checkHit(note.hitRange[3])) {
      JUDGE_LINE.setGrade(3);
      this.act = false;
    }
  }
(略)
  close() {
    this.act = false;
    this.show = false;
  }
}

図18 「js/game/gameScoreManager.js」ファイルに追加するコード

(略)
  calcScore(grade) {
    switch (grade) {
      case 0:
        this.point += 100 + this.combo;
        this.perfect++;
        this.combo++;
        break;
      case 1:
        this.point += 80 + this.combo * 0.8;
        this.great++;
        this.combo++;
        break;
      case 2:
        this.point += 50 + this.combo * 0.5;
        this.good++;
        this.combo++;
        break;
      case 3:
        this.bad++;
        this.combo = 0;
        break;
    }
    if (this.maxCombo < this.combo) {
      this.maxCombo = this.combo;
    }
  }
(略)

図19 「js/game/backLane.js」ファイルに追加するコード

(略)
  judge() {
(略)
      if (GRADE[i] < 3) {
        // Bad以外の判定ならヒットSEを鳴らす
        sound.playSE(sound.seList[0]);   
      } else {
        sound.playSE(sound.seList[1]); // Bad判定ならバッドSEを鳴らす
      }
      gameScore.calcScore(GRADE[i]); // スコアを更新
      JUDGE_LINE.setGrade(GRADE[i]); // ノーツヒットのグレードを描画
      TARGET[i].close();             // 判定済みのノーツ処理を停止
    }
  }
(略)

図20 「js/game/singleNote.js」ファイルに追加するコード

class SingleNote {
(略)
  update() {
(略)
    if (this.reachTime < time.elapsed &&
        !this.checkHit(note.hitRange[3])) {
      gameScore.calcScore(3);
      JUDGE_LINE.setGrade(3);
      this.act = false;
    }
  }
(略)
}

図22 「js/game/backLane.js」ファイルに追加するコード

(略)
  draw() {
(略)
    if (inputKey.status[this.key]) {
      const GRAD = CTX.createLinearGradient(this.x, 
                     JUDGE_LINE.centerY, this.x, CANVAS_H / 3);
      GRAD.addColorStop(0.0, 'rgba(' + this.actColor + ', 0.6)');
      GRAD.addColorStop(1.0, 'rgba(' + this.actColor + ', 0)');
      CTX.fillStyle = GRAD;
      CTX.fillRect(this.x, 0, this.width, JUDGE_LINE.centerY);
    }
  }
(略)