
走れない代わりに、走るゲームを創るプロジェクト、第4回。
過去のバックナンバーは以下のとおり。
- #1 横移動・ジャンプ動作
- #2 敵・ダメージ・スコア
- #3 ジャンプ動作・敵ランダム出現・ゲームオーバー表示
今回は主に
- 走るアニメーションの追加
- 地平線の追加
- タップで再開機能
ようやく“走ってる感”が出てきたぞ!
本日の成果物
- STARTボタンで開始
- ゲーム画面クリック(タップ)または[H]キーで再開
とにかく走ってるアクションが反映できたのがウレシイ。
本日のドット絵
今回ロックマンが走っているドット絵は、以前Excelでパラパラ漫画をつくったときの画像をそのまま流用した。
走るロックマン

おさらいではあるが、Excelからpngファイルを抽出するには
- ドット打ちしたセルをコピー
- 形式を画像として貼り付け
- 画像をコピー
- 形式をPNGとして貼り付け
- xlsxをzipにリネーム
- xl
- media
- 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フレームごとに走るモーションが切り替える。
いや、その考え方はね、分かってたんだけどね…
ちょっと恥ずかしいミスを犯してたんだよね。
本日の学び
今まで走るモーションが実現できなかった原因は、ホントお恥ずかしい話
プログラミングしてるとこういうことって往々にしてある。
大文字と小文字が違ったりね。
なぜか自分だけ気づかなかったりするのだ。
今後の展望
ゲームの骨子ができあがってきたので、だんだん操作性とか画質とかが気になり始めてきた。
STARTボタンを廃止してクリック(またはタップ)で始められるようにするとか。
背景がちゃんと動いてるようにみせる、とか。
やりたいことは尽きない。
走る、を創ろう。