あっぷりノート

Fix the Bits | あっぷり工房

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

#4 JavaScriptでアクションゲームを作る │ 走る動作、地平線、タップして再開

f:id:you_key69:20220215233946j:plain


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

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

今回は主に

  • 走るアニメーションの追加
  • 地平線の追加
  • タップで再開機能
のバージョンアップを施した。

ようやく“走ってる感”が出てきたぞ!


本日の成果物
















  • STARTボタンで開始
  • ゲーム画面クリック(タップ)または[H]キーで再開

とにかく走ってるアクションが反映できたのがウレシイ。

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

本日のドット絵

今回ロックマンが走っているドット絵は、以前Excelでパラパラ漫画をつくったときの画像をそのまま流用した。

走るロックマン

f:id:you_key69:20220215233922j:plain

おさらいではあるが、Excelからpngファイルを抽出するには

  1. ドット打ちしたセルをコピー
  2. 形式を画像として貼り付け
  3. 画像をコピー
  4. 形式をPNGとして貼り付け
  5. xlsxをzipにリネーム
  6. xl
  7. media
  8. image*.png

という流れで実現可能である。

PNG透過

そういえばExcelで作ったpngファイルの透過処理の仕方を紹介していなかった。

透過処理はPEKO STEPさんのWebサービスのお世話になっている。

pngファイルをWebページにドラッグ&ドロップして、透明色にしたい色をクリックするだけ。

実にカンタンでベンリである。


この場をお借りして御礼申し上げたい(あざっす)。

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

今回のポイントは3枚の走るアクション画像だが、特にいったんすべて配列(ImgArray)に格納してから呼び出す、という知恵を活かせた自分をホメてあげたいとおもう。

<input type="button" onclick="start()" value="START" id="btn_Restart">
<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;

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

// キャラクター画像を配列に格納
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"
	};

onload = function () {
  // 描画コンテキストの取得
  canvas = document.getElementById("gamecanvas");
  g = canvas.getContext("2d");
  
  // 初期化
  //init();
  
  // 入力処理の指定
  document.onkeydown = keydown;
  document.onmousedown = mousedown;
  
  // ゲームループの設定(FPS)
  setInterval("gameloop()", 16);
};
// スタートボタン押下
function start() {
	init();
}

// リスタート
function restart() {
	if (scene == Scenes.GameOver) {
		init();
	}
}

function init() {

	// キャラクター表示位置・スピード・半径
	characterPosX = 0;
	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 = "https://cdn-ak.f.st-hatena.com/images/fotolife/y/you_key69/20220211/20220211222731.png";
	enemySpeed = 5;
	
	// スコア初期化
	score = 0;
	frameCount = 0;
	scene = Scenes.GameMain;
}

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

// マウスクリック(タップ)
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.GameMain) {
		speed = speed + acceleration;
		characterPosY = characterPosY + speed;
		
		// 初期移動 
		if (characterPosX < 70) {
			characterPosX = characterPosX + 5; // 1フレーム毎に2ドット移動
		}
		if (characterPosY > 300) {
			characterPosY = 300; // 着地
			speed = 0;
			acceleration = 0;
		}
		
		// 敵が左端まで行ったら戻る
		enemyPosX -= enemySpeed;
		if (enemyPosX < -100) {
			enemyPosX = 210 + Math.floor(Math.random() * 210); // ランダムに出現
			score += 100; // 避けたら100pt加算
		}
		
		// 走る動作
		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;
		}
		
		// フレームカウントリセット
		if (frameCount == 40) {
			frameCount = 0;
		}
				
		// 当たり判定
		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;
		}
	}
}

function draw() {
	// 背景描画
	g.fillStyle = "rgb(51,204,153)";
	//g.fillStyle = "rgb(153,204,51)";
	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);
	
	// キャラクタ描画
	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 or Press [H] to Restart";
		var overLabelWidth = g.measureText(overLabel).width;
		g.fillText(overLabel, 210 - overLabelWidth / 2, 220); // 表示文言,位置指定(x,y)

	}
}
</script>

あと、地平線を分けるのは単純に、ひとつのキャンバスに長方形を2つ並べる方式で実現した。

これも天才だとおもっている。


しかしながら、とにかく動くことを重視していたせいで、いよいよソースがスパゲッティ(複雑で冗長化)になってきた。

どこかのタイミングでキレイにしなければ。

本日の教材

教材はこれ↑


フレームカウントを増やして、10フレームごとに走るモーションが切り替える。

いや、その考え方はね、分かってたんだけどね…


ちょっと恥ずかしいミスを犯してたんだよね。

本日の学び

今まで走るモーションが実現できなかった原因は、ホントお恥ずかしい話

else if を if else と書いていた
という、凡ミス(#゚Д゚)

プログラミングしてるとこういうことって往々にしてある。


大文字と小文字が違ったりね。

なぜか自分だけ気づかなかったりするのだ。

今後の展望

ゲームの骨子ができあがってきたので、だんだん操作性とか画質とかが気になり始めてきた。


STARTボタンを廃止してクリック(またはタップ)で始められるようにするとか。

背景がちゃんと動いてるようにみせる、とか。


やりたいことは尽きない。

走る、を創ろう。