あっぷりノート

Fix the Bits | あっぷり工房

旅、ギア、サプリ、マインド、トレーニング ── “走る”は創れる

#5 JavaScriptでアクションゲームを作る │ タップdeスタート、登場シーン、徐々にスピードアップ

f:id:you_key69:20220216223240j:plain


走れない代わりに、走るゲームを創るプロジェクト、第5回。

過去のバックナンバーは以下のとおり。

今回は主に

  • タップしてスタート
  • 登場シーン追加
  • 徐々にスピードアップ
のバージョンアップを施した。

操作性、ゲーム性、そして“ロックマンらしさ”が格段にアップしたゾ。


本日の成果物















  • スタート・再開
    • ゲーム画面をクリック(タップ) または [H]キー押下

スタートボタンを廃止して、画面タップでのスタートに変更した。

やはり現代はスマホでの操作に最適化していないといけないと思うのだ。

画面外のタップ(クリック)でもジャンプ可能です。
ゲーム画面上をタップしてジャンプすると敵に衝突してすぐにリスタートしてしまうことがあるので、画面外タップでのジャンプを推奨します。

本日のドット絵

ロックマン好きとしてはできるだけロックマンの所作も本家に寄せたいものである。

ということで、登場シーンもバージョンアップした。

登場するロックマンA

f:id:you_key69:20220216222535j:plain

登場するロックマンB

f:id:you_key69:20220216222544j:plain

登場するロックマンC

f:id:you_key69:20220216222553j:plain

登場シーンの動作

動きとしては単純に[A]→[B]→[C]ではなく、

  1. [A] を上から降らせる
  2. 着地時に[B]に変更
  3. 一瞬だけ[A]に変更
  4. 最後に[C]に変更
  5. ランニング開始

という動作になる。


━━というのをYouTubeでロックマンプレイ動画を0.25倍速にして何度もチェックした。

だから合ってるはず。

本日のソースコード(JavaScript)

1000点毎にスピードを1上げるようにしてみた。

あと、レベルが上がった感を演出するために、地味に背景色を暗くするという工夫もこらしてみたが、いかがだろうか。

<canvas id="gamecanvas" width="420" height="360" onclick="restart()"></canvas>

<script>
var canvas, g;
var characterPosX, characterPosY, characterImage, characterR;
var enemyPosX, enemyPosY, enemyImage, enemySpeed, enemyR;
var speed,acceleration;
var score;
var scene;
var frameCount;
var darken; // 画面の暗さ

// シーンを配列に格納
const Scenes = {GameMain: "GameMain", GameOver: "GameOver", GameOpen: "GameOpen"};

// キャラクター画像を配列に格納
const ImgArray = {
	 CharaRun1: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220211/20220211130115.png"
	,CharaRun2: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220213/20220213165305.png"
	,CharaRun3: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220213/20220213165308.png"
	,CharaJump: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220213/20220213004026.png"
	,CharaWarp1: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220216/20220216000325.png"
	,CharaWarp2: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220216/20220216000328.png"
	,CharaWarp3: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220216/20220216000332.png"
	,Enemy1: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220211/20220211222731.png"
	};

onload = function () {
	// 描画コンテキストの取得
	canvas = document.getElementById("gamecanvas");
	g = canvas.getContext("2d");

	// 初期化
	scene = "";
	darken = 0;

	// 入力処理の指定
	document.onkeydown = keydown;
	document.onmousedown = mousedown;

	// ゲームループの設定(FPS)
	setInterval("gameloop()", 16);
};

// 画面クリックしてスタート
function restart() {
	if (scene != Scenes.GameMain && scene != Scenes.GameOpen) {
		scene = Scenes.GameOpen;
		open();
	}
}
function open() {	
	// キャラクター表示位置・スピード・半径
	characterPosX = 70;
	characterPosY = 0;
	characterImage = new Image();
	characterImage.src = ImgArray.CharaWarp1;
	
	enemyImage = new Image();
	enemyImage.src = "";
	
	// スコア初期化
	score = 0;
	frameCount = 0;
	darken = 0;
}

function init() {
	// キャラクター表示位置・スピード・半径
	characterPosX = 70;
	characterPosY = 300;
	characterR = 22;
	characterImage = new Image();
	characterImage.src = ImgArray.CharaRun1;
	speed = 0;
	acceleration = 0;

	// 敵の表示位置・スピード
	enemyPosX = 630;
	enemyPosY = 308;
	enemyR = 18;
	enemyImage = new Image();
	enemyImage.src = ImgArray.Enemy1;
	enemySpeed = 5;
	
	frameCount = 0;
	scene = Scenes.GameMain;
}

// キー押下
function keydown(e) {
	if (e.keyCode == 74) { // [J]押下時
		characterJump();
	}
	else if (e.keyCode == 72) { // [H]押下時
		restart();
	}
}

// マウスクリック(タップ)
function mousedown() {
	if (scene == Scenes.GameMain) {
		characterJump();
	}
}
function characterJump() {
	if (characterPosY == 300) {
		speed = -20; // Y軸の移動スピード
		acceleration = 1.2; // 加速度(重力)
	}
}

function gameloop() {
	update(); // キャラクターの移動
	draw(); // キャラクターの描画
}

function update() {
	frameCount++;
	
	if (scene == Scenes.GameOpen) {
		
		// 着地するまで下移動
		if (characterPosY <= 280) {
			characterPosY = characterPosY + 10;
			frameCount = 0;
		}
		else {
			//着地モーション
			if (frameCount <= 5 ) {
				characterImage.src = ImgArray.CharaWarp2;
			}
			else if (frameCount <= 10 ) {
				characterImage.src = ImgArray.CharaWarp1;
			}
			else if (frameCount <= 15 ) {
				characterImage.src = ImgArray.CharaWarp3;
			}
			else {
				characterImage.src = ImgArray.CharaRun1;
				
				// ランニングスタート!
				init();
			}
		}
	}
	else if (scene == Scenes.GameMain) {
		speed = speed + acceleration;
		characterPosY = characterPosY + speed;
		
		if (characterPosY > 300) {
			characterPosY = 300; // 着地
			speed = 0;
			acceleration = 0;
		}
		
		// 敵が左端まで行ったら戻る
		enemyPosX -= enemySpeed;
		if (enemyPosX < -100) {
			enemyPosX = 240 + Math.floor(Math.random() * 180); // ランダムに出現
			score += 100; // 避けたら100pt加算
			enemySpeed = 5 + Math.floor(score / 1000); // 1000点毎にスピードアップ
			darken = Math.floor(score / 1000) * -20; // 1000点毎に背景色暗くする
		}
		
		// 走る動作
		if (characterPosY == 300) {
			if (frameCount <= 10 ) {
				characterImage.src = ImgArray.CharaRun1;
			}
			else if (frameCount <= 20 ) {
				characterImage.src = ImgArray.CharaRun2;
			}
			else if (frameCount <= 30 ) {
				characterImage.src = ImgArray.CharaRun3;
			}
			else {
				characterImage.src = ImgArray.CharaRun2;
			}
		}
		// ジャンプ動作
		else {
			characterImage.src = ImgArray.CharaJump;
		}
		
		// 当たり判定
		var diffX = characterPosX - enemyPosX;
		var diffY = characterPosY - enemyPosY;
		var distance = Math.sqrt(diffX * diffX + diffY * diffY);
		if (distance < characterR + enemyR) {
			enemySpeed = 0;
			characterImage.src = "";		
			scene = Scenes.GameOver;
		}
		
		// フレームカウントリセット
		if (frameCount == 40) {
			frameCount = 0;
		}
	}
}

function draw() {
	// 背景描画
	var red = 76 + darken;
	var green = 229 + darken;
	var blue = 178 + darken;
	var rgb = "rgb(" + red + "," + green + "," + blue + ")";
	g.fillStyle = rgb;
	//g.fillStyle = "rgb(51,204,153)";
	g.fillRect(0, 0, 420, 326);

	g.fillStyle = "rgb(188,153,51)";
	g.lineWidth = 3;
	g.strokeStyle = 'black';
	g.strokeRect(0, 326, 420, 34);
	g.fillRect(0, 326, 420, 34);
	
	// 初期画面は文言の表示のみ
	if (scene == "") {
		g.fillStyle = "rgb(255,255,255)";
		g.font = "12pt Arial";
		var overLabel = "Click Here or Press [H] to Start";
		var overLabelWidth = g.measureText(overLabel).width;
		g.fillText(overLabel, 210 - overLabelWidth / 2, 220); // 表示文言,位置指定(x,y)
	}
	else {
		// キャラクタ描画
		g.drawImage(
			characterImage,
			characterPosX - characterImage.width / 2,
			characterPosY - characterImage.height / 2
		);
		
		// 敵描画
		g.drawImage(
			enemyImage,
			enemyPosX - enemyImage.width / 2,
			enemyPosY - enemyImage.height / 2
		);

		// スコア描画
		g.fillStyle = "rgb(255,255,255)";
		g.font = "16pt Arial";
		var scoreLabel = "SCORE : " + score;
		var scoreLabelWidth = g.measureText(scoreLabel).width;
		g.fillText(scoreLabel, 390 - scoreLabelWidth, 40); // 表示文言,位置指定(x,y)

		if (scene == Scenes.GameOver) {

			// ゲームオーバー表示
			g.fillStyle = "rgb(255,255,255)";
			g.font = "24pt Arial";
			var overLabel = "GAME OVER";
			var overLabelWidth = g.measureText(overLabel).width;
			g.fillText(overLabel, 210 - overLabelWidth / 2, 180); // 表示文言,位置指定(x,y)

			g.fillStyle = "rgb(255,255,255)";
			g.font = "12pt Arial";
			var overLabel = "Click Here or Press [H] to Restart";
			var overLabelWidth = g.measureText(overLabel).width;
			g.fillText(overLabel, 210 - overLabelWidth / 2, 220); // 表示文言,位置指定(x,y)

		}
	}
}
</script>

本日の学び

プログラミングというより、操作性やゲーム性、クオリティを重視してブラッシュアップを施してみた。

JavaScriptとして新たな技術を取り入れたということはないが、実際ゲームをプレイする立場からすれば、そんなのはどうだっていいこと。


いかに楽しんでもらえるかが大事なので。

スキル偏重にならず、ユーザー目線を大切にしていきたい。

今後の展望

「GAME OVER」してすぐに画面上をタップするとすぐにリスタートしてスコアが消えてしまうという問合せをいただいたので、なんとかしたい。

運用でカバーできるとはいえ、システムで防げるなら防ぎたいものである。


あと、背景が素っ気ないので、なんとかしたい。

あとは新しい敵特殊武器を追加したりとステージクリアの要素も追加していきたい。

走る、を創ろう。