本連載について
- プログラミング初心者がスーパーマリオ的なゲームを作成するのに情報をまとめたものです
- 不明点や不備あれば、なんでもコメントいただけると大変嬉しいです!!!
より良いものにしたいので! - 一番最初の連載はこちらから確認お願いします!
▼ゲームイメージ
▼目次
[1章 準備する](https://qiita.com/hockeyarchitecture/items/139c27ff4806eaf2367f)
[2章 簡単なページ作ってみる](https://qiita.com/hockeyarchitecture/items/76e0e90c1883d91b3e87)
[3章 画像を動かしてみる](https://qiita.com/hockeyarchitecture/items/c526e2345cb512109e18)
本章の概要
- 簡易ステージ作って落ちたらゲームオーバーになるとこまでいきます
- 細かな制御や敵にぶつかったらゲームオーバーになったりはこの章以降やります
- 本章の内容は大きく4ステップです
- その1 〜ジャンプさせてみるの巻〜
- その2 〜着地判定させてみるの巻〜
- その3 〜ブロック上に着地しないと落ちちゃうよの巻〜
- その4 〜ジャンプしないと落ちちゃうよの巻〜
- その5 〜簡易ステージ作るよの巻〜
- その6 〜ゲームオーバーになっちゃうよの巻〜
- 各ステップごとに実際のソースをQiita上に記載しています
- 上記と同じくソースの実態を保存しているgitのリポジトリも記載しています
リンクにアクセスして実際のソースをダウンロードすることができます
ぜひダウンロードして動かしながら試してみてください!
その1 〜ジャンプさせてみるの巻〜
ゴール
- 上を押したら、上に飛び上がり、一定時間したら下に落下するようにします
前提
- 上下を押した際に左右同様に一定速度で動くのでなく、実際のジャンプっぽい動きにします
- 一応自由落下に関する学術的な説明を簡単に記載しておきます
詳細を理解することは必須ではないです
今の座標 に 速度 を足して 次の座標 を出すことができるってことだけ認識してもらえれば問題ないです
- 高校物理で習うことですが、現実世界で初速度ありの水直方向の速度は、上向きをプラスとして以下で表現されます
vy = v0 - g × t
vy : 垂直方向の速度
v0 : 初速度
g : 重力加速度という定数(9.8)
t : 経過した時間(秒) - ただし
canvas
要素上では上方向がマイナスで±逆転指定しまうので、それを考慮して以下に修正します
vy = - v0 + g × t
つまりは初速度はマイナスで与えてあげて、時間が経過する度に速度を少しづつ増やせば良いことになります -
t
秒後のy座標は今の座標に 速度×t
秒で計算することができます
(例1) 3mのポイントにある物体が、1秒に2mの速度を持つ場合、1秒後には3 + 2 × 1
で5mのポイントにいることがわかります
(例2) 5mのポイントにある物体が、1秒に-1mの速度を持つ場合、1秒後には5 + (-1) × 1
で4mのポイントにいることがわかります
つまりは今の座標に速度を足して次の座標を出してあげれば良いことになります
(ここで速度が変動することに注意してください) - 細かい説明は省きますが時間(数式での
t
)は常に一定であるため速度の値を調整しプログラム上
のロジックには記載しなくて良いようにしています
- 高校物理で習うことですが、現実世界で初速度ありの水直方向の速度は、上向きをプラスとして以下で表現されます
やること
- 上下方向の速度を初期値0で定義する
(canvas
要素の定義に合わせて下向きがプラスとします) - 上ボタンを押したら上下方向の速度をマイナスの固定値とします
(ここでは、固定値の値は、厳密に現実世界に則った数値でなく、実際に動かしてそれっぽく動くものとします) - ジャンプ中の場合には、上下方向の速度を少しづつ増やします
(ここで増やす速度の値は、厳密に現実世界に則った数値でなく、実際に動かしてそれっぽく動くものとします) - 前のy座標に上下方向の速度を足して次のy座標を計算します
▼ソース
// キーボードの入力状態を記録する配列の定義
var input_key_buffer = new Array();
// キーボードの入力イベントをトリガーに配列のフラグ値を更新させる
window.addEventListener("keydown", handleKeydown);
function handleKeydown(e) {
e.preventDefault();
input_key_buffer[e.keyCode] = true;
}
window.addEventListener("keyup", handleKeyup);
function handleKeyup(e) {
e.preventDefault();
input_key_buffer[e.keyCode] = false;
}
// canvas要素の取得
const canvas = document.getElementById("maincanvas");
const ctx = canvas.getContext("2d");
// 画像を表示するの座標の定義 & 初期化
var x = 0;
var y = 300;
// 上下方向の速度
var vy = 0;
// ジャンプしたか否かのフラグ値
var isJump = false;
// ロード時に画面描画の処理が実行されるようにする
window.addEventListener("load", update);
// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
// 画面全体をクリア
ctx.clearRect(0, 0, 9999, 9999);
// 入力値の確認と反映
if (input_key_buffer[37]) {
// 左が押されていればx座標を1減らす
x = x - 2;
}
if (input_key_buffer[38]) {
// 上が押されていれば、上向きの初期速度を与え、ジャンプ中のフラグを立てる
vy = -7;
isJump = true;
}
if (input_key_buffer[39]) {
// 右が押されていればx座標を1増やす
x = x + 2;
}
// ジャンプ中である場合のみ落下するように調整する
if (isJump) {
// 上下方向は速度分をたす
y = y + vy;
// 落下速度はだんだん大きくなる
vy = vy + 0.5;
}
// 主人公の画像を表示
var image = new Image();
image.src = "../images/character-01/base.png";
ctx.drawImage(image, x, y, 32, 32);
// 再描画
window.requestAnimationFrame(update);
}
※ 実際のソースコードは こちら からダウンロードできます
▼ CodePenのサンプル (主人公消えたら右下の"return"をクリックして、リフレッシュして確認してみてください)
See the Pen mario-game-tutorial-01-04-01 by taku7777777 (@taku7777777) on CodePen.
説明
- 主なロジックの流れは以下のようにしています
- 変数(x座標、y座標、上下方向の速度、ジャンプ中か否かのフラグ値)を初期化して定義します
- updateの関数内で、入力値に応じて更新します
- updateの関数内で、ジャンプ中の場合は速度とy座標の調整します
- updateの関数内で、更新したx座標・y座標で画像を描画します
- 上ボタンを押した際の初速度は
-7
としています - ジャンプ中に増やす速度は
0.5
としています - このままだとジャンプはするが地面がなくずっと落下したままになってしまいます
追加対応はその2で!
その2 〜着地判定させてみるの巻〜
ゴール
- 地面追加して、ジャンプした後に着地できるようにします
前提
-
drawImage
で指定しているy座標は画像の左上の座標です - 下
- 画像下部の座標は
指定しているy座標 + 画像の高さ
となります - ですので地面の画像のy座標は
300 + 32
と指定すれば良い事がわかります
やること
- 地面の画像ファイルを作成し、配置します ([参考]ドッド絵を作成する)
- 地面の画像を描画します
- 主人公画像の下部のy座標が地面画像の上部のy座標を超えた場合に地面に着地させます
- 着地時には、ジャンプ中状態の解除と、主人公のy座標を地面の上に立っているように見える位置に更新をします
- フォルダ階層は以下ようにしています
└┬─ src ┬─ index.html
│ └─ index.js
└─ images ┬─ character-01 ── base.png
└─ ground-01 ── base.png
▼ ソース (その1との差分を抜粋して記載)
...(省略)
// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
...(省略)
// ただし画像下部が地面の上部より下にはいかないようにする
if (y + 32 > 332) {
y = 332 - 32;
}
// 主人公の画像を表示
var image = new Image();
image.src = "../images/character-01/base.png";
ctx.drawImage(image, x, y, 32, 32);
// 地面の画像を表示
var groundImage = new Image();
groundImage.src = "../images/ground-01/base.png";
ctx.drawImage(groundImage, 0, 300 + 32, 640, 32);
// 再描画
window.requestAnimationFrame(update);
}
※ 実際のソースコードは こちら からダウンロードできます
▼ CodePenのサンプル
See the Pen mario-game-tutorial-01-04-02 by taku7777777 (@taku7777777) on CodePen.
説明
- 主人公が地面より下にいる場合の条件は
主人公の下部(y座標 + 高さ)
>地面の上部(y座標)
- 主人公が地面の上に見えるようなy座標は
地面の上部(y座標)
+主人公の高さ
- 着地できるようにしましたが、これでは逆に地面がなくて落ちてしまうことがなくなっていまいました...
- 地面がなかったら落下しちゃうようにする対応はその3で対応します
その3 〜ブロック上に着地しないと落ちちゃうよの巻〜
ゴール
- ジャンプした後に、ブロック(地面)があれば着地し、なければそのまま落下を続けるようにします
前提
- ロジック簡易化のためここではブロックは全て同じ高さにあるものとします
- ブロックの高さがいろんな高さにある場合の対応はその5で行います
やること
- ブロックの位置を複数定義します
特にx座標とその幅に注意して、意図した"穴"を作るようにします - 主人公の画像が地面より下になるタイミングで、いづれかのブロックの上にいる場合には着地、いづれの地面の上にもいない場合には落下するようにします
(例1) ブロック1の上にいないが、ブロック2の上にいる場合 → ブロック2に着地
(例2) ブロック1の上にいない、ブロック2の上にいない場合 → そのまま落下 - ブロックの上にいない条件は、
主人公の右端がブロックの左端より左
または主人公の左端がブロックの右端より右
になります - ブロックの上部を通過する前後でブロックの上にいなければ落下、ブロックの上にいれば着地とします
▼ ソース (その2から差分のみを記載)
...(省略)
// 上下方向の速度
var vy = 0;
// ジャンプしたか否かのフラグ値
var isJump = false;
// ブロック要素の定義
var blocks = [
{ x: 0, y: 332, w: 200, h: 32 },
{ x: 250, y: 332, w: 200, h: 32 },
{ x: 500, y: 332, w: 530, h: 32 }
];
// ロード時に画面描画の処理が実行されるようにする
window.addEventListener("load", update);
// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
// 画面全体をクリア
ctx.clearRect(0, 0, 640, 480);
// 更新後の座標
var updatedX = x;
var updatedY = y;
// 入力値の確認と反映
if (input_key_buffer[37]) {
// 左が押されていればx座標を1減らす
updatedX = x - 2;
}
if (input_key_buffer[38]) {
// 上が押されていれば、上向きの初期速度を与え、ジャンプ中のフラグを立てる
vy = -7;
isJump = true;
}
if (input_key_buffer[39]) {
// 右が押されていればx座標を1増やす
updatedX = x + 2;
}
// ジャンプ中である場合のみ落下するように調整する
if (isJump) {
// 上下方向は速度分をたす
updatedY = y + vy;
// 落下速度はだんだん大きくなる
vy = vy + 0.5;
// 主人公の画像下部が地面の上部より下となったタイミングでブロックの上にいるか否かの判定をする
if (y + 32 < 332 && updatedY + 32 >= 332) {
// 全てのブロックに対して繰り返し処理をする
for (const block of blocks) {
if (
(x + 32 < block.x || x >= block.x + block.w) &&
(updatedX + 32 < block.x || updatedX >= block.x + block.w)
) {
// ブロックの上にいない場合には何もしない
continue;
}
// ブロックの上にいる場合にはジャンプ解除し、y座標をブロックの上にいるように見える位置にする
updatedY = 300 + 32 - 32;
isJump = false;
break;
}
}
}
x = updatedX;
y = updatedY;
// 主人公の画像を表示
var image = new Image();
image.src = "../images/character-01/base.png";
ctx.drawImage(image, x, y, 32, 32);
// 地面の画像を表示
var groundImage = new Image();
groundImage.src = "../images/ground-01/base.jpeg";
for (const block of blocks) {
ctx.drawImage(groundImage, block.x, block.y, block.w, block.h);
}
// 再描画
window.requestAnimationFrame(update);
}
※ 実際のソースコードは こちら からダウンロードできます
▼ CodePenのサンプル
See the Pen mario-game-tutorial-01-04-03 by taku7777777 (@taku7777777) on CodePen.
説明
- 着地チェックをする際に変更前の座標と変更後の座標を使用するため新しく変更後の座標を一時的に格納しておくための変数を追加します
-
var blocks = [{ x: 0, y: 332, w: 200, h: 32 }, ... ]
ではJSON形式
で定義した要素を、配列で複数定義できるようにしています -
配列
:[A, B, C]
のように[]
と,
で要素を複数指定することができます
(こちらやこちらなどに、他に詳しく記載されている記事があるのでそちらをご覧ください) -
JSON形式
:{name: value}
のように{}
でname: value
の塊を,
区切りで指定することができます
name
を指定することでvalue
を取得することができます
(こちらなどに、他に詳しく記載されている記事があるのでそちらをご覧ください) - 複数のブロックに対して着地チェックや描画処理をする際に
for(const block of blocks){...}
で繰り返し処理をしています
これでblocks
内の全要素に対して{...}
の部分に記載した処理を実施することができます
continue
で途中で処理を切り上げて次の要素の処理に映る事ができます
break
で繰り返し処理自体を途中で終了させる事ができます
(こちらなどに、他に詳しく記載されている記事があるのでそちらをご覧ください) - ジャンプして着地するブロックがない場合の落下は実現できましたが、ジャンプせずに横移動した際の落下は対応できていないです
ここは次のその4で対応します - だいぶネストが深くなってきましたが、今は全体像の理解が優先のためリファクタリングは後回しとします
その4 〜ジャンプしないと落ちちゃうよの巻〜
ゴール
- ジャンプせずに横移動してブロックがなくなった場合に、落下するようにします
前提
- ロジック簡易化のためここではブロックは全て同じ高さにあるものとします
- ブロックの高さがいろんな高さにある場合の対応はその5で行います
やること
- ジャンプ中でない場合もブロックの上にいるか否かのチェックを行うようにすします
▼ ソース (その3から差分のみを記載)
... (省略)
// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
... (省略)
// ジャンプ中である場合のみ落下するように調整する
if (isJump) {
... (省略)
} else {
// いづれかのブロックの上にいるかをチェックする
var isOnBlock = false;
for (const block of blocks) {
if (
(x + 32 < block.x || x >= block.x + block.w) &&
(updatedX + 32 < block.x || updatedX >= block.x + block.w)
) {
// ブロックの上にいない場合には何もしない
continue;
}
// ブロックの上にいる場合にはフラグを立てる
isOnBlock = true;
break;
}
// ブロックの上にいなければジャンプ中の扱いとして初期速度0で落下するようにする
if (!isOnBlock) {
isJump = true;
vy = 0;
}
}
x = updatedX;
y = updatedY
... (省略)
※ 実際のソースコードは こちら からダウンロードできます
▼ CodePenのサンプル
See the Pen mario-game-tutorial-01-04-04 by taku7777777 (@taku7777777) on CodePen.
説明
-
isOnBlock
の新たな変数を追加していづれのブロックの上にもいなかった場合に落下するようにしています - 本当は
blocks.filter(block => {...})
とか使えばもうちょっとカッコよくコーディングできるのですがここでは読みやすさ重視としています - ブロックの上にいるかの分岐が重複しているのですが、ここはその5できれいにします
- 異なる高さのブロックの対応はその5で対応します
その5 〜簡易ステージ作るよの巻〜
ゴール
- 重複してしまっているブロックの上にいるか否かの分岐をきれい(リファクタリング)します
- 異なる高さのブロックにも対応できるようにします
前提
- リファクタリングはコードを無駄を省き読みやすくする事で 非常に 重要な作業です
- これを放っておくと、ソースが読みにくくなる他、追加で修正しようとした時に影響する範囲を読み間違えてバグを生み出す原因となります
やること
- ブロックの上にいるか否かのロジックを切り出します
ロジックの切り出しを行う際には、inputとして何が必要で、outputとして何が必要か整理しましょう - ジャンプ中にブロックの上にいるか否かの判定ロジックでは、
inputとして必要なものは変更前のxy座標
変更後のxy座標
outputとして必要なものはブロックの上にいるか否か
ブロックの上にいる場合はそのブロックの情報
- ジャンプ中でない際にブロックの上にいるか否かの判定ロジックでは、
inputとして必要なものは変更前のx座標
変更後のx座標
outputとして必要なものはブロックの上にいるか否か
- 上記を踏まえて、切り出す関数は以下のようにします
input :変更前のxy座標
変更後のxy座標
output :ブロックの上にいる場合はそのブロックの情報、いない場合はNull
-
null
はデータがないことを表す定数のようなものです
▼ ソース (その4から差分のみを記載)
... (省略)
// ブロック要素の定義
var blocks = [
{ x: 0, y: 332, w: 200, h: 32 },
{ x: 250, y: 232, w: 200, h: 32 },
{ x: 500, y: 132, w: 530, h: 32 }
];
// ロード時に画面描画の処理が実行されるようにする
window.addEventListener("load", update);
// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
... (省略)
// ジャンプ中である場合のみ落下するように調整する
if (isJump) {
// 上下方向は速度分をたす
updatedY = y + vy;
// 落下速度はだんだん大きくなる
vy = vy + 0.5;
// 主人公が乗っているブロックを取得する
const blockTargetIsOn = getBlockTargrtIsOn(x, y, updatedX, updatedY);
// ブロックが取得できた場合には、そのブロックの上に立っているよう見えるように着地させる
if (blockTargetIsOn !== null) {
updatedY = blockTargetIsOn.y - 32;
isJump = false;
}
} else {
// ブロックの上にいなければジャンプ中の扱いとして初期速度0で落下するようにする
if (getBlockTargrtIsOn(x, y, updatedX, updatedY) === null) {
isJump = true;
vy = 0;
}
}
x = updatedX;
y = updatedY;
... (省略)
}
// 変更前後のxy座標を受け取って、ブロック上に存在していればそのブロックの情報を、存在していなければnullを返す
function getBlockTargrtIsOn(x, y, updatedX, updatedY) {
// 全てのブロックに対して繰り返し処理をする
for (const block of blocks) {
if (y + 32 <= block.y && updatedY + 32 >= block.y) {
if (
(x + 32 <= block.x || x >= block.x + block.w) &&
(updatedX + 32 <= block.x || updatedX >= block.x + block.w)
) {
// ブロックの上にいない場合には何もしない
continue;
}
// ブロックの上にいる場合には、そのブロック要素を返す
return block;
}
}
// 最後までブロック要素を返さなかった場合はブロック要素の上にいないということなのでnullを返却する
return null;
}
※ 実際のソースコードは こちら からダウンロードできます
▼ CodePenのサンプル
See the Pen mario-game-tutorial-01-04-05 by taku7777777 (@taku7777777) on CodePen.
説明
- ブロック要素のy座標を変更しています
- 変更前後のxy座標を渡したら、乗っかっているブロック要素を返してくれる共通関数を定義・使用するように修正しています
- 一番下まで落下したらゲームオーバーとするロジックはその6で!
その6 〜ゲームオーバーになっちゃうよの巻〜
ゴール
- ブロックから落下して下まで落ちてしまった場合にゲームオーバーにするようにします
- ゲームオーバーとなったら簡易ダイアログを表示し、最初の状態に戻します
やること
- ゲームオーバーか否かのフラグ値を定義します
- y座標が一定の値を超えた場合にゲームオーバーとします
- ゲームオーバーとなったらゲームオーバーようの画像で表示するようにします
▼ ソース (その5から差分のみを記載)
... (省略)
// ゲームオーバーか否かのフラグ値
var isGameOver = false;
// ブロック要素の定義
var blocks = [
{ x: 0, y: 332, w: 200, h: 32 },
{ x: 250, y: 232, w: 200, h: 32 },
{ x: 500, y: 132, w: 530, h: 32 }
];
// ロード時に画面描画の処理が実行されるようにする
window.addEventListener("load", update);
// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {
// 画面全体をクリア
ctx.clearRect(0, 0, 640, 480);
// 更新後の座標
var updatedX = x;
var updatedY = y;
if (isGameOver) {
// 上下方向は速度分をたす
updatedY = y + vy;
// 落下速度はだんだん大きくなる
vy = vy + 0.5;
if (y > 500) {
// ゲームオーバーのキャラが更に下に落ちてきた時にダイアログを表示し、各種変数を初期化する
alert("GAME OVER");
isGameOver = false;
isJump = false;
updatedX = 0;
updatedY = 300;
vy = 0;
}
} else {
... (省略)
if (y > 500) {
// 下まで落ちてきたらゲームオーバーとし、上方向の初速度を与える
isGameOver = true;
updatedY = 500;
vy = -15;
}
}
x = updatedX;
y = updatedY;
// 主人公の画像を表示
var image = new Image();
if (isGameOver) {
// ゲームオーバーの場合にはゲームオーバーの画像が表示する
image.src = "../images/character-01/game-over.png";
} else {
image.src = "../images/character-01/base.png";
}
ctx.drawImage(image, x, y, 32, 32);
... (省略)
}
... (省略)
※ 実際のソースコードは こちら からダウンロードできます
▼ CodePenのサンプル
See the Pen mario-game-tutorial-01-04-06 by taku7777777 (@taku7777777) on CodePen.
説明
- ソースは見ての通り
- ちょっとゴチャってきてしまいましたね
- これでだいぶゲームっぽくなってきたのではないでしょうか?
- ここまできたら一応ゲームと読んでも良いのかな?クリアの定義がないですが...
終わりに
お疲れまです!!!
次に進みましょう!!!
Now Creating ...