こんにちは。ゲームプログラマーのメガネです。
「JavaScriptで脱初心者のために、少し複雑なゲームを作ってみたい」
という人を対象にしています。じゃんけんにタイミング要素を追加した、リズムじゃんけんというゲームを作りました。ゲームの作り方を解説します。
JavaScript初心者の人は、まずはこちらを見ていってください。
リズムじゃんけんを公開
ゲームを遊ぶ
上からじゃんけんボールが降ってきます。
画面下のラインとボール中央のラインが一致するタイミングで、「✊」「✌️」「🖐️」のどれかを選んでください。
ボールに「かって」と書かれていたらボールに勝つ手を選び、「まけて」と書かれていたら負ける手を、「あいこ」と書かれていたらあいこになる手を選びます。
ライン同士が一致するタイミングで押せたら高得点です。
段々難しくなっていきますので、高得点を目指してがんばってください!
スマホ等でフルスクリーンで遊びたい場合はこちら。
https://meganeprog.com/js-samples/rythm-janken/rythm-janken.html
ソースコード
ソースコード一式はGitHubで公開しています。ファイル名をクリックすればブラウザでも見れるので、気軽に覗いてください。
https://github.com/meganeprog/javascript_rythm_janken
ファイルの説明:
- rythm-janken.html・・・ゲームの側のHTMLです。ほとんど空っぽです。
- rythm-janken.js・・・このゲームのJavaScriptコードです。
- framework.js・・・rythm-janken.jsで利用するフレームワークのソースコードです。
ソースコードの説明
自作の簡易フレームワーク
自作の簡易フレームワークを使用しています。以下の記事で紹介していますので、見ていってください。
じゃんけんボールをスポーン
こちらがボールをスポーンする関数です。
#spawnBall(context) {
this.#time += context.deltaTime;
this.#nextBallTimer -= context.deltaTime;
if (this.#nextBallTimer < 0) {
// じゃんけんボールの出現タイミング
this.#nextBallTimer = 0;
// レベルテーブルの time が 0 じゃないときは、一定時間おきにレベルアップする
if (LEVEL_OF_DIFFICULTY_TABLE[this.#level].time != 0) {
if (this.#time > LEVEL_OF_DIFFICULTY_TABLE[this.#level].time) {
if (this.#balls.getChildCount() == 0) {
this.#time = LEVEL_OF_DIFFICULTY_TABLE[this.#level].time;
++this.#level;
}
else {
return;
}
}
}
// 現在のレベル
const level = LEVEL_OF_DIFFICULTY_TABLE[this.#level];
// かちまけのルールを決める
// 設定された割合に応じてランダムに決定
const rand = Math.random() * (level.win + level.lose + level.draw);
const rule = rand < level.win ? Rule.win : (rand < level.win + level.lose ? Rule.lose : Rule.draw);
// じゃんけんボールの手をランダムに決める
const hands = [Hand.rock, Hand.scissors, Hand.paper];
const hand = hands[Math.floor(Math.random() * 3)];
// 移動速度を設定
const speed = this.#judgementSizes.good * level.speed;
// じゃんけんボールをスポーン
this.#balls.addChild(new Ball(rule, hand, speed, this.#judgementSizes.good, this.#judgementSizes.excellent));
// スポーン間隔のタイマーをリセット
this.#nextBallTimer = level.interval;
}
}
JavaScriptの上の方でレベルテーブルを作成しています。
const LEVEL_OF_DIFFICULTY_TABLE = [
{time: 7, interval: 3, speed: 3, win:1, draw:0, lose:0},
{time: 14, interval: 3, speed: 4, win:0, draw:0, lose:1},
{time: 21, interval: 3, speed: 5, win:0, draw:1, lose:0},
{time: 30, interval: 2, speed: 5, win:1, draw:1, lose:1},
{time: 40, interval: 2, speed: 7, win:1, draw:1, lose:1},
{time: 50, interval: 1.5, speed: 8, win:1, draw:0, lose:0},
{time: 60, interval: 1.5, speed: 8, win:0, draw:0, lose:1},
{time: 70, interval: 1.5, speed: 8, win:0, draw:1, lose:0},
{time: 80, interval: 1.7, speed: 10, win:1, draw:1, lose:1},
{time: 90, interval: 1.2, speed: 3, win:1, draw:1, lose:1},
{time: 100, interval: 1.4, speed: 8, win:1, draw:1, lose:1},
{time: 0, interval: 1.2, speed: 10, win:1, draw:1, lose:1},
];
このテーブルの指示どおりに、じゃんけんボールを出現させるようにしています。
// 現在のレベル
const level = LEVEL_OF_DIFFICULTY_TABLE[this.#level];
// かちまけのルールを決める
// 設定された割合に応じてランダムに決定
const rand = Math.random() * (level.win + level.lose + level.draw);
const rule = rand < level.win ? Rule.win : (rand < level.win + level.lose ? Rule.lose : Rule.draw);
// じゃんけんボールの手をランダムに決める
const hands = [Hand.rock, Hand.scissors, Hand.paper];
const hand = hands[Math.floor(Math.random() * 3)];
// 移動速度を設定
const speed = this.#judgementSizes.good * level.speed;
// じゃんけんボールをスポーン
this.#balls.addChild(new Ball(rule, hand, speed, this.#judgementSizes.good, this.#judgementSizes.excellent));
// スポーン間隔のタイマーをリセット
this.#nextBallTimer = level.interval;
一定時間おきにレベルアップしていきます。
// レベルテーブルの time が 0 じゃないときは、一定時間おきにレベルアップする
if (LEVEL_OF_DIFFICULTY_TABLE[this.#level].time != 0) {
if (this.#time > LEVEL_OF_DIFFICULTY_TABLE[this.#level].time) {
if (this.#balls.getChildCount() == 0) {
this.#time = LEVEL_OF_DIFFICULTY_TABLE[this.#level].time;
++this.#level;
}
else {
return;
}
}
}
じゃんけんボールの処理
こちらがじゃんけんボールのクラスです。
class Ball extends fw.GameObject {
constructor(rule, hand, speed, radius, lineWidth) {
super();
this.#rule = rule;
this.#hand = hand;
this.#speed = speed;
this.#radius = radius;
this.#lineWidth = lineWidth;
// ルールによって色を変える
if (rule == Rule.win) {
this.#color = "indianred";
}
else if (rule == Rule.lose) {
this.#color = "royalblue";
}
else {
this.#color = "mediumseagreen";
}
this.#y = -radius;
}
onUpdate(context) {
this.#y += context.deltaTime * this.#speed;
}
onRender(context) {
const x = Math.floor(context.canvas.width / 2);
const y = Math.floor(this.#y);
this.#x = x;
context.beginPath();
context.arc(x, y, this.#radius, 0 * Math.PI / 180, 360 * Math.PI / 180, false);
context.fillStyle = this.#color;
context.fill();
context.fillStyle = "rgb(255, 255, 255)";
context.fillRect(x-this.#radius, y - this.#lineWidth / 2, this.#radius*2, this.#lineWidth);
context.font = `${Math.floor(this.#radius/3)}pt sans-serif`;
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText(this.#rule, x, y - this.#radius / 2);
context.font = `${Math.floor(this.#radius/2)}pt sans-serif`;
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText(this.#hand, x, y + this.#radius / 2);
}
get x() {
return this.#x;
}
get y() {
return this.#y;
}
get radius() {
return this.#radius;
}
get hand() {
return this.#hand;
}
get rule() {
return this.#rule;
}
#x = 100;
#y = 100;
#radius;
#lineWidth;
#rule;
#hand;
#color;
#speed;
}
更新処理はシンプルです。単純に速度を縦方向に足していて、上から下に落としているだけです。
onUpdate(context) {
this.#y += context.deltaTime * this.#speed;
}
ボタンが押されたときの判定
ボタンが押されたときの判定です。
「👊」「✌️」「🖐️」それぞれのボタンに、judgement
というコールバックを仕込んでいます。ボタンが押されたらコールバックが呼ばれます。
this.addChild(new Button(this.controller.canvasWidth/4, this.controller.canvasHeight/10*9, this.#judgementSizes.good, Hand.rock, (hand) => { this.#judgement(hand); }));
this.addChild(new Button(this.controller.canvasWidth/4*2, this.controller.canvasHeight/10*9, this.#judgementSizes.good, Hand.scissors, (hand) => { this.#judgement(hand); }));
this.addChild(new Button(this.controller.canvasWidth/4*3, this.controller.canvasHeight/10*9, this.#judgementSizes.good, Hand.paper, (hand) => { this.#judgement(hand); }));
「かって」「まけて」「あいこ」の選択に成功していたら、ボールと判定ラインの距離に応じて「エクセレント」「グレート」「グッド」の評価になります。
選択に失敗していたらミスです。ミスの場合は体力を1減らしていて、体力が0になるとゲームオーバーです。
また、ボールが判定ラインよりも上にあるときには、ボタンが押されても無視しています。じゃないと厳しすぎますからね。
#judgement(hand) {
if (this.#balls.getChildCount() > 0) {
// 一番下のボールだけを判定
let ball = this.#balls.getChild(0);
// ボールと判定ラインの距離の差分
const dy = this.#judgementLine - ball.y;
if (dy > this.#judgementSizes.good) {
// 判定距離よりも遠いので無視
}
else {
// 成功したかどうか
const success = this.#checkHand(ball.rule, hand, ball.hand);
// 成功していてかつ、距離の差分がエクセレントの範囲内
if (success && dy >= -this.#judgementSizes.excellent && dy <= this.#judgementSizes.excellent) {
// エクセレント
this.addChild(new Particle(ball.x, ball.y, ball.radius, "gold"));
this.addChild(new ExcellentEffect());
ball.removeSelf();
this.#score += this.#points.excellent;
}
// 成功していてかつ、距離の差分がグレートの範囲内
else if (success && dy >= -this.#judgementSizes.greate && dy <= this.#judgementSizes.greate) {
// グレート
this.addChild(new Particle(ball.x, ball.y, ball.radius, "silver"));
this.addChild(new GreateEffect());
ball.removeSelf();
this.#score += this.#points.greate;
}
// 成功していてかつ、距離の差分がグッドの範囲内
else if (success && dy >= -this.#judgementSizes.good && dy <= this.#judgementSizes.good ) {
// グッド
this.addChild(new Particle(ball.x, ball.y, ball.radius, "cadetblue"));
this.addChild(new GoodEffect());
ball.removeSelf();
this.#score += this.#points.good;
}
else {
// ミス
ball.removeSelf();
this.addChild(new MissEffect());
if (--this.#life == 0) {
// ゲームオーバー
this.controller.pushScene(new GameOverScene(this.controller));
}
}
}
}
}
ボタンを押せずに行きすぎてしまったときの判定
落下しすぎたボールがあれば、体力を1減らしてミスにします。体力が0になったらゲームオーバーです。
// 行き過ぎたものを削除する
this.#balls.childForEach((ball) => {
if (ball.y > this.controller.canvasHeight/5*4 + this.#judgementSizes.good) {
this.addChild(new MissEffect());
ball.removeSelf();
if (--this.#life == 0) {
// ゲームオーバー
this.controller.pushScene(new GameOverScene(this.controller));
}
}
});
演出
成功・失敗した際に、演出を入れています。
成功したときにParticle
クラスを生成して、ボタンから円を放射しています。
ExcellentEffect
は「エクセレント」という文字を、GreateEffect
は「グレート」、GoodEffect
は「グッド」と表示しています。
ミスのときはMissEffect
です。
パッと作るために、似たようなクラスがいっぱいできてしまったのは反省点です。
まとめ
JavaScriptで作る少し複雑なゲーム「リズムじゃんけん」の作り方を紹介しました。
レベルテーブルを変更することで、難易度を変えることができます。色々いじって遊んでもらえるとうれしいです。
コメント