p5.jsでミニゲーム「メテオロイド」を作ってみた


2018年04月15日
With
p5.jsでミニゲーム「メテオロイド」を作ってみた はコメントを受け付けていません

前回のエントリでは、こちらの本の第1章「ベクトル」で紹介されているProcessingコードをp5.jsに移植しながらアレンジしたのですが、

■ Nature of Code – Processingではじめる自然現象のシミュレーション

その画面が「弾幕シューティング」のようになったのに触発されて、せっかくなのでよりゲームぽくアレンジしたものを作ることに挑戦してみました。シンプルですが、それなりに楽しくできたと思うので、よかったら遊んでみてください。

●ミニゲーム「メテオロイド」(クリックすると別タブが開きます。マウス推奨。ちょっとだけ音が出るので音量注意)

起動すると即ゲームスタートです。黒いカンバス上のどこかをクリックすると、そこに「赤い二重丸」が表示されます。ゲームの目的は、画面上でマウスポインタを動かし、この「赤い二重丸」をゆっくりと追いかけてくる青いツブツブとぶつからないよう、できるだけ長く逃がし続けてやることです。

ツブツブには、常にポインタの位置に向かって引っ張られるように力が働きます。また、ポインタとツブツブの距離が近いほど、強い力が働きます。

スタート時、ツブツブは画面上に1個しかありませんが、中央でカウントアップしているタイマーで約10秒ごとに倍に増えていきます。約70秒で128個まで増えて、そこで増加は止まります。逃げ切れず、赤い二重丸の中心部にツブツブがぶつかってしまうと、画面上にスタートから衝突時までの時間が表示され、ゲームオーバーです。もう一度遊びたいときには「F5」キーなどでリロードしてください。

…自分でも遊んでみたのですが、普通にやると「1分30秒」を超えられません(笑)。分裂のタイミングでの位置取りを考えて、なるべくツブツブの動く方向をそろえるようにすると長く生き残れる…かな。いろいろ試してもらえると嬉しいです。

「タイマー」を使おうとしてハマる

今回、いろいろなサイトを参考にしながら「getTime()」という関数を作り、「Date」クラスを使ってスケッチにタイマーを表示したり、時間によって難易度が上がるような仕組みを入れることにチャレンジしてみました。

しかし、これが例によって一筋縄にはいかず。たとえば「10秒ごとにツブツブの数が倍になる」という処理を作るために、最初、起動からの経過時間を「Date.now()-startTime」でとって監視し、それが「ぴったり10000(ミリ秒)の倍数」になったときにレベルアップのフラグを立てるというのを考えたのですが、このやり方だと、スケッチの処理負荷によって、監視のタイミングと「ぴったり10秒」のタイミングがずれてしまい、フラグが立ってほしいときに立たなかったり、立たなくていいときに立ちまくったりしてしまうのですね。

別の案として「draw()」の実行回数をカウントする変数を別に作っておいて、その値でレベルアップフラグを立てることも考えたのですが、これも負荷によって変わってしまう以上、実時間と大きくズレてしまう。できれば「10秒ごと」にはこだわりたい。

そこで、今回のスケッチでは妥協策として「経過時間が10の倍数秒から+1秒までのタイミングでレベルアップフラグを立てて、1度立てたら、次の10の倍数秒がくるまでは再度フラグを立てない」という、すさまじく泥臭い処理を入れて「約10秒に1回のツブツブ倍増」を実現しました。…こういうのって、もっとうまいやり方があるんでしょうかね?

2020年5月5日追記:このエントリを書いてから数年後。JavaScriptには「setInterval」という、そのものズバリの挙動をしてくれる便利な関数があることを知りました。こういう基本的なことに気付くのに時間がかかるというのが独学のツラいところ…

そのほかにも、デジタルタイマーで「分、秒、100分の1秒をそれぞれ2ケタで表示する」とか、ゲームオーバー処理のところとかに、いろいろとこれまでのスケッチでは使ったことがない処理を入れてみました。こんな感じのミニゲーム、今後もちょくちょく作ってみたいです。

ちょっと長くなりますが、コードは以下のとおりです。一部ES2015準拠で、音声ファイルのハンドリングのために「p5.sound.js」を追加ライブラリとして使っています。また、画面に表示しているフォントはGoogleのWebフォント「Anton」です。

const meteorCount = 1
const maxMeteors = 128;
let meteors, me;
let startTime, nowMinStr, nowSecStr, nowMilSecStr;
let tmpLvupTime = 0, lvupTime = false;
let fo = 200;
let stageAlpha = .2;
let crashSE;

function preload() {
	crashSE = loadSound('crash.mp3');
}

function setup() {
	createCanvas(600, 600);
	textFont("Anton");
	colorMode(HSB);
	background(0);
	meteors = Array.from(new Array(meteorCount).fill(new Meteor(random(width), random(height))));
	me = new Me();
	startTime = Date.now();
}

function draw() {
	noStroke();
	fill(0, 0, 0, stageAlpha);
	rect(0, 0, width, height);
	if (me.isAlive) {
		for (let i = 0; i < meteors.length; i++) {
			meteors[i].update();
			meteors[i].checkEdges(i);
			meteors[i].disp(225, 22, 100);
		}
		me.checkEdges();
		me.disp(0, 50, 100);
		checkLocs();
		getTime();
		if (lvupTime) { lvup(); }
	}
	if (!me.isAlive) {
		if (fo >= 0) {
			meteors.forEach(meteo => { meteo.endSq(fo); });
			fo -= 2;
		}
		me.disp(240, 40, 40);
		if (fo < 0) {
			noStroke();
			fill(0, 0, 100, 1);
			textSize(80);
			text('GAME OVER', 130, height / 2 - 90);
			text(nowMinStr, 165, height / 2 + 40);
			text(':' + nowSecStr, 250, height / 2 + 40);
			text(':' + nowMilSecStr, 350, height / 2 + 40);
			textSize(40);
			text('RELOAD TO TRY AGAIN', 152, height / 2 + 140);
			noLoop();
		}
	}
}

function checkLocs() {
	for (var i = 0; i < meteors.length; i++) {
		if (dist(me.loc.x, me.loc.y, meteors[i].loc.x, meteors[i].loc.y) < me.rad / 4) {
			stageAlpha = 1;
			me.isAlive = false;
			crashSE.play();
		}
	}
}

function lvup() {
	let nowLength = meteors.length;
	for (let i = 0; i < nowLength; i++) {
		meteors.push(new Meteor(meteors[i].loc.x, meteors[i].loc.y))
	}
	lvupTime = false;
}

class Meteor {
	constructor(ex, why) {
		this.accel;
		this.loc = createVector(ex, why);
		this.vel = createVector(0, 0);
		this.rad = 8;
		this.topsp = 3.8;
	}
	update() {
		var mouse = createVector(mouseX, mouseY);
		var dir = p5.Vector.sub(mouse, this.loc);
		var magn = p5.Vector.mag(dir);
		dir.normalize();
		dir.mult(10 / magn);
		this.accel = dir;
		this.vel.add(this.accel);
		this.vel.limit(this.topsp);
		this.loc.add(this.vel);
	}
	disp(h, s, b) {
		noStroke();
		fill(h, s, b, 1);
		ellipse(this.loc.x, this.loc.y, this.rad);
	}
	checkEdges(i) {
		if (i < 64) {
			if (this.loc.x > width) {
				this.loc.x = 0;
			} else if (this.loc.x < 0) {
				this.loc.x = width;
			}
			if (this.loc.y > height) {
				this.loc.y = 0;
			} else if (this.loc.y < 0) {
				this.loc.y = height;
			}
		} else {
			if (this.loc.x > width) {
				this.loc.x = width;
			} else if (this.loc.x < 0) {
				this.loc.x = 0;
			}
			if (this.loc.y > height) {
				this.loc.y = height;
			} else if (this.loc.y < 0) {
				this.loc.y = 0;
			}
		}
	}
	endSq(fo) {
		for (let rotAngle = 0; rotAngle < 360; rotAngle += 45) {
			push();
			translate(this.loc.x, this.loc.y);
			rotate(radians(rotAngle));
			noStroke();
			fill(random(180, 300), 100, 100, fo / 100);
			ellipse(0, 105 - fo / 2, this.rad * map(fo, 200, 0, 10, 1));
			pop();
		}
	}
}

class Me {
	constructor() {
		this.loc = createVector(0, 0);
		this.rad = 15;
		this.isAlive = true;
	}
	checkEdges() {
		var mX = mouseX, mY = mouseY;
		if (mX > width) {
			mX = width;
		} else if (mX < 0) {
			mX = 0;
		}
		if (mY > height) {
			mY = height;
		} else if (mY < 0) {
			mY = 0;
		}
		this.loc = createVector(mX, mY);
	}
	disp(h, s, b) {
		fill(h, s, b, 1);
		noStroke();
		ellipse(me.loc.x, me.loc.y, this.rad);
		noFill();
		stroke(h, s, b, 1);
		strokeWeight(2);
		ellipse(me.loc.x, me.loc.y, this.rad + 7);
	}
}

function getTime() {
	let justnow = Date.now() - startTime;
	let nowMin = floor(justnow / 1000 / 60);
	let nowSec = floor(justnow / 1000 % 60);
	let nowMilSec = floor((justnow - nowMin * 60000 - nowSec * 1000) / 10);
	nowMinStr = setzero(nowMin);
	nowSecStr = setzero(nowSec);
	nowMilSecStr = setzero(nowMilSec);
	if (nowSec % 10 == 0 && nowSec != tmpLvupTime) {
		if (meteors.length < maxMeteors) { lvupTime = true; }
		tmpLvupTime = nowSec;
	}
	noStroke();
	fill(0, 0, 100, .02);
	textSize(80);
	text(nowMinStr, 165, height / 2 + 40);
	text(':' + nowSecStr, 250, height / 2 + 40);
	text(':' + nowMilSecStr, 350, height / 2 + 40);
}

function setzero(num) {
	let addzero;
	if (num < 10) {
		addzero = "0" + num;
	} else { addzero = num; }
	return addzero;
}