あっぷりノート

Fix the Bits | あっぷり工房

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

#6 JavaScriptでアクションゲームを作る │ ダメージシーン追加

f:id:you_key69:20220217221408j:plain


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

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

今回は

  • ダメージシーン追加
のバージョンアップを施した。

たった1つの追加であるが、一石二鳥のパフォーマンスに成功したのであるッ!


本日の成果物















画面上をタップしてジャンプ前に敵に当たってしまうと、タップが再スタートと認識されリセットされてしまう不都合があった。

画面外のタップでしのいでいただいていたが、それでは不親切すぎる。


ということで、その問題を解決すべく

敵に当たった後、ホンモノさながらにダメージを受ける
というアクションを追加した。

これにより、

  • よりホンモノの動きに近づける
  • 即時タップを受けつけなくする
という、2つのメリットを同時に実現することに成功した。


これはもはや"ジーニアス"としか形容しようがない(自己満^^)。

本日のドット絵

今回、新規追加したのはもちろん肝であるダメージシーンである。

ダメージを受けるロックマン

f:id:you_key69:20220217221149j:plain

あいかわらずExcelでのドット打ちではあるが、画像サイズを15%に縮小してからpng化することで、画像劣化させずに透過処理ができることを発見した。


その気づきにあわせて、地味にジャンプするロックマンの画質も上げてみた。

透過処理がヘタすぎて気になっていたので、キレイになって満足である。

本日の教材

できるだけ本家に寄せるためにロックマンのプレイ動画をくり返し凝視している。

その中でもよくお世話になる動画を紹介しよう。


どちらもBGMがカッコ良すぎる、というのも推しポイントだ。

ランニングにふさわしい180bpmの楽曲である。

ダメージシーン

ロックマン2』より「ウッドマンステージ

YouTubeにはノーダメージ動画ばかりが上がっているので、なかなかダメージを受けるモーションに遭遇しにくい。

こちらは終盤にダメージを受けてくれる貴重な映像である。

登場シーン

ロックマン4』より「スカルマンステージ

個人的にいちばん好きなロックマン4。

ステージも曲もかっこいい。何より「スカル」というのが心をくすぐるw

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

やり方が正しいのか分からないが、ダメージシーンを追加し、そこで

  • 画像変更
  • ロックマンが後退する
  • 3回点滅させる
  • 消えてからもx軸=0地点まで後退させる

という手法で、重複タップができない時間を設けるように工夫した。

<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; // 画面の暗さ
var damageCount;

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

// キャラクター画像を配列に格納
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/20220216/20220216232026.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"
	,CharaDamage: "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220216/20220216232029.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 == "" || scene == Scenes.GameOver) {
	//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;
	damageCount = 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;
			*/
			
			//characterImage.src = ImgArray.CharaDamage;
			//damageCount = frameCount;
			scene = Scenes.GameDamage;
		}
		else {
			// フレームカウントリセット
			if (frameCount == 40) {
				frameCount = 0;
			}
		}
	}
	// ダメージシーン(かつ超過タップによるリセット防止)
	else if (scene == Scenes.GameDamage) {
		// 後方へ戻す
		characterPosX = characterPosX - 1;
		// 点滅させる
		damageCount ++;
		if (damageCount <= 5 || (damageCount > 10 && damageCount <= 15) || (damageCount > 20 && damageCount <= 25)) {
			characterImage.src = ImgArray.CharaDamage;
		}
		else {
			characterImage.src = "";
		}
		// ゲームオーバーシーンへ
		if (characterPosX == 0) {
			scene = Scenes.GameOver;
		}
	}
}

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>

相変わらずコードが長ったらしくて申し訳ない。

今後の展望

今回の問題は実際にプレイいただいた方からの声を元に対応させていただいた。

この動作は想定していたものの、実際にレスポンスをいただくと何とかして解消したいと思うものである。


バージョンアップはプレイして頂けることに対する恩返しみたいなものだ。

いつもありがとうございます。


今後も貴重なプレイヤーを大切にし、ユーザーフレンドリーでいきたいとおもう。

走る、を創ろう。