初心者向け!3時間でミニゲームを作ろう(2)

2022/03/21に公開

前回の記事:https://zenn.dev/takahashi112211/articles/de8f053ac70719

壁を追加する

ここで、ミニゲームに壁を追加します。
前回は、小人の作り方を学びました。それと同じように、壁面を作りましょう。

mygame.create.wall()

上の図のように、ゲーム画面の左下にグレーの四角が表示されます。 左下にあった小さな小人が消えました。これは、小人とグレーの四角が同じ場所にあり、グレーの四角が小人を覆っているからです。
とりあえず小人を無視して、残りの壁を作ることを考えよう。
ご覧のように、これがゲームに必要な壁です(濃いグレーの部分)。

壁はそれぞれ異なる位置を持つので、作成時に壁の位置を指定する必要があります。
mygame.create.wall(0, 1)
// 0は壁の座標x、1はその座標y

この壁が位置を変えて、小人の頭の上の1フレームになったことにお気づきでしょう。
よし、ここまでもうお分かりでしょう。前の図面と同じ、適切な場所に壁を作ればいいのです。
例えば、

mygame.create.wall(0, 0)
mygame.create.wall(0, 1)
mygame.create.wall(0, 2)
mygame.create.wall(0, 3)
mygame.create.wall(0, 4)
mygame.create.wall(0, 5)
mygame.create.wall(0, 6)
mygame.create.wall(0, 7)
mygame.create.wall(0, 8)
mygame.create.wall(0, 9)

そして

一挙に10枚の壁が完成!しかし、これは遅すぎではないか?
先ほどの図面の壁を数えると、外周だけで18 * 2 + 30 * 2 = 96枚の壁があり、これを書いたら手が攣りそうです。
この問題を解決するために、「ループ」というツールを導入します。
上の10枚の壁を作るコードを見ると、そのパターンが発見できます。すべての壁のx座標は同じ(すべて0)で、yは0から増加し、yが9になるまで毎回1を加えます。
このプロセスを短いコードで説明できます。

for (let i = 0; i <= 9; i++) {
    mygame.create.wall(0, i)
}

その意味は:iを作成して0とし、括弧{ }の内容を実行するたびに、iに1を加算します。そして、毎回括弧の内容を実行する時、i小なりイコール9かどうかをチェックし、この条件が満たされないと、実行しません。
全サイクルの内訳は以下の通りです。

let i = 0
問題:i <= 9 か?
答え:はい
実行:mygame.create.wall(0, 0)
i = 1
問題:i <= 9 か?
答え:はい
実行:mygame.create.wall(0, 1)
i = 2
問題:i <= 9 か?
答え:はい
実行:mygame.create.wall(0, 2)
i = 3
......
......
i = 9
問題:i <= 9 か?
答え:はい
実行:mygame.create.wall(0, 9)
i = 10
問題:i <= 9 か?
答え:いいえ

サイクルが終了します
この道具を覚えれば、壁を作るのが楽になります。 上の図面に従って、外周に壁を作ろう。

for (let i = 0; i <= 17; i++) {
   mygame.create.wall(0, i)
   mygame.create.wall(29, i)
}
for (let i = 0; i <= 29; i++) {
   mygame.create.wall(i, 0)
   mygame.create.wall(i, 17)
}

あとは図面通りに壁を作るだけです。もし何かに行き詰まったら、メモ帳でexample.htmlを開いてヒントを探してみてください。

human.x = 1
human.y = 1

そうしたら、彼が見えます!

星と炎

その直後に、星と炎を加えます。 まずは図面を見ましょう。

星や炎の位置はわかりやすいので、あとは適所でブロックを作り、パターンを設定すればいいのです。しかし、いざコーディングに取りかかろうとすると、「どのようなブロックを作ればいいのか、星型や炎型のブロックはあるのでしょうか?」という問題が出てきます。
例えば、こんな風に書けるかな?

mygame.create.star(28, 4)
mygame.create.fire(14, 3)

このように書いて、コードを保存してページを更新したときに、ゲーム画面に星や炎が期待通りに表示されません。ブロックの中に星型ブロックや炎型ブロックがないためです。
今何をしたらいいかわからないかもしれないが、落ち着いて考えましょう。
ゲームに加えたい星の特性は何ですか?
あるいは小人を星に近づけて操ったとき、どの位置で小人が星を食べるのしょうか?
一般的に、小人が通り過ぎるときに、その星と重なって食べられます。
つまり、壁と違って、星は小人に通過されます。
ここまで、「小人に通過されるって、さっきの空気型のブロックと同じじゃないか!」と思われたかもしれませんね。
もう一度、前回の分析を振り返ってみましょう。最も基本的なブロックは何ですか?

  1. 操作できる人
  2. 人が踏み越えられない壁
  3. 人が自由に動ける空気
    現在のシーンでは、追加したい星の性質は空気ブロックが持っているものと同じです。
    つまり、星を作るために「星」型のブロックを用意する必要はなく、既存のブロックを使って、星の性質に合うようなものを作ることができるのです。このように、空気型のブロックを使って星を作られます。
let star1 = mygame.create.air(28, 4)
star1.style = pattern.star


そして、図面を参照して他の星を作り、同じ方式で炎を作ること考えましょう。問題が発生した場合は、example.htmlをメモ帳で開いてヒントを確認します。

弾丸を飛ばしましょう

弾丸を追加する前に、弾丸の飛行経路を解析してみましょう。
ゲームを体験したと、赤い銃口で発生した弾丸がゲーム画面の左端まで飛んでいき、消えていくのがわかります。

こうして、結論が出ます。弾丸を銃口で生成させ、ゲーム画面の外に出るまでずっと左に移動させます。1秒に1発の弾丸が生成します(希望する弾丸密度に応じて、ここでは1秒間隔にしていますが、この間隔は自由に変更可能です)。
まず、弾が1つしか生成しない場合を考えてみましょう。
銃口で弾丸が生成されるコードは、あなたはこう書くかもしれません。

let bullet = mygame.create.air(28, 1)
bullet.style = pattern.bullet

次に、左に動かし続けることです。 その前に、ブロックが提供する自動移動機能について知っておく必要があります。自動移動とは、設定した距離を一定の方向に、設定した速度でブロックを通過させることです。説明書でのautomove.leftの説明を読むと、自動移動機能の詳細を知ることができます。

bullet.automove.left(29, 4)

上のコードで、29は弾丸が左に移動する距離、4は弾丸が移動する速度です。 保存してページを更新すると、弾丸が飛ぶ効果を確認することができます。
1発の弾丸の飛ぶ問題さえ解決すれば、あとは銃口の位置で1秒ごとに弾丸を生成し、弾丸を「automove」させれば、すべてが完成します。
1秒ごとに弾丸を生成させることを実現したいと、簡単に考えて、もしタイマーがあれば、1秒ごとにトリガーして、以下のコードが実行します。

let bullet = mygame.create.air(28, 1)
bullet.style = pattern.bullet
bullet.automove.left(29, 4)

それがいいじゃない? 要は、そんなタイマーがあるのか。

まあ、ありますよ! 次のように書く。

setInterval(function() {
    // コードを書く
}, 1000)

上記のコードを理解するためには、関数を知る要がある。

私たちの多くは、中学数学の「y = kx + b」から関数の初歩的な知識を持っています。ここで、k と b は定数、x は独立変数、y は従属変数です。 x の入力が異なれば、異なる y または同じ y がられる(この例では、異なる y しか得られない)。
xを入力するとyが出力されるという点では同じ構造であるが、プログラミング言語には、xもyも持ち得ない関数も存在する点が異なる。上記のタイマーのコードのように、単に手続き(コード)を実行して渡すための入れ物として使われる。

function() {
    // 実行されるコード
}

内部で実行したいコードを書き、この関数と実行間隔をタイマーに伝える(渡す)と、タイマーは指定した間隔でこの関数内のコードを実行します。

次のように書きましょう。

let createbullet = function() {
    let bullet = mygame.create.air(28, 1)
    bullet.style = pattern.bullet
    bullet.automove.left(29, 4)
}
setInterval(createbullet, 1000)

それともこう書いてもいい

setInterval(function() {
   let bullet = mygame.create.air(28, 1)
   bullet.style = pattern.bullet
   bullet.automove.left(29, 4)
}, 1000)

ここで、英語カンマの後の1000は1000msを意味します(1秒=1000ms)。全コードの意味は、タイマーに1000msに1回関数内のコードを実行させる——弾丸を作成し、速度4で左に29マス移動させる - ということです。
最後に、もう1つの銃口を追加してみましょう。

let gun = mygame.create.wall(28, 1)
gun.style = pattern.gun

こうして、弾丸発射の機能が完成しました!
小さな練習:付録のパターンイラストと説明書を参考に、あの飛んでいる緑の亀甲を作ってください。 行き詰まったときは、メモ帳でexample.htmlを開いてヒントを参照してください。

多様な危険

先ほどから弾丸や炎などの危険なアイテムを追加しましたが、自分でプレイしてみると、これらは小人にとって致命的ではないことがわかります。
私たちの予想では、小人が弾、炎や亀甲に触れるとゲームが終了します。 そのロジックをわかりやすく言うと、下の図のように

まず、1つ目のフレームから見ていきます。「小人は炎に触れるか」って、コードでどう書きますか?
答えは、条件文を使うことです。

if (xxx) {
    //  ここに条件を満たしたときに実行されるコードを書く
} else {
    // ここに条件を満たさないときに実行されるコードを書く
}

ifの後の括弧内の「xxx」は判定条件であり、もしその判定条件の結果がtrue、つまり「はい、その通りです」であれば、最初の括弧内のコードが実行されます。
判定条件の結果がfalse、つまり「いいえ」場合は、elseにはいて、2番目の括弧内のコードが実行されます。
前の文で見たように、判定条件は「人が炎に触れたかどうか」であり、つまりこの部分の疑似コードは次のように書くべきです。

if (人が炎に触れた) {
    // ここで人が炎に触れた後に実行すべきコードを書く
}

上の擬似コードでは、小人が炎に触れないとゲームが続くので「else」を使っておらず、「ゲームが続く」ということは、現在の場合では「何も起こらない」と考えます。
では、「小人が炎に触れた」はどのように表現するのでしょうか。 私たちのブロックは、istouchedというアクションを提供しており、このブロックが他のブロックと接触したかどうかを判断することに使用します。接触した場合はそのアクションが「true」で、その逆は「 false」です。
だから、この部分は次のように書きます。

if (human.istouched(fire1)) {
    //  人が火に触れたときに実行されるコードをここに書く
}

ブロックには、istouchedの他に、isoverlappingというアクションがあります。それはブロックが重なっているかどうかを判断するために使用されることです。この二つのアクションの具体的な違いについては、説明書で探してみましょう。
続けて、「人が炎に触れたか? 」
そうであれば、ゲームは終了です。 また、ゲームではゲームを終了させるためのアクションも用意されています。

mygame.over.lose() // gameoverで負けを宣告
mygame.over.win() // gameoverで勝利を宣告

そして、この過程をコードで表すと、次のようになります。

if (human.istouched(fire1)) {
   mygame.over.lose()
}

では、この「小人」と「炎」が接触しているかどうかを確認する行為は、いつ行われるのだろうか。
コンピュータから見ると、プレイヤーの操作する小人がいつ炎に接触するか分からず、接触したかどうか常に検出することで、実際に炎に接触した時に正しいフィードバックができるのです。
以前使っていたタイマーを使って、上記の検出コードをタイマーに入ればどう?と思われたかもしれません。
では、「常に」というのは、どのように定義すればいいのでしょうか。 1秒に1回?あるいは0.5秒ごと? 0.01秒ごと?
実際、コンピュータはプレイヤーのキー操作の頻度を知らないので、どのような間隔を選んでも適切ではありません。そして、「常に」というのは、時間の次元ではなく、小人の動きの次元で考えます。
つまり、頻度ではなく、小人が動くたびに炎との接触を検知することが重要なのです。 これにより、プレイヤーの動きが「速い」「遅い」に関わらず、効果的に検出することができます。
私たちのブロックは、この「常に」を実現するための方法を提供します。

human.move.after = function() {
    // ここに小人が動いたときに実行されるコードを書く
    // 小人が動くたびに、コードが一度実行される
}

したがって、「小人が動く→炎と接触したか判断→YESならゲーム終了/NOならゲーム続行」という処理を実現できます。

human.move.after = function() {
   if (human.istouched(fire1)) {
       mygame.over.lose()
   }
}

亀甲(ここではデフォルトで亀甲となるブロックはturtleと呼ばれる)と炎も同じように実装されており、引き続きその検出をhuman.move.after関数に追記すればよいのです。

human.move.after = function() {
    if (human.istouched(fire1)) {
        mygame.over.lose()
    }
    if (human.istouched(fire2)) {
        mygame.over.lose()
    }
    if (human.isoverlapping(turtle)) {
        mygame.over.lose()
    }
}

だから、今はもう弾丸しか残っていない。
小人と弾が重なるかどうかを検出する方法は、前と少し異なります。
これは、ゲームの実行に伴って新しい弾が生成されるため、一度書いたコードを保存して実行すると自動的に修正できません。即ち、human.move.after関数に、弾丸が追加する際に小人と弾丸が重なるかどうかを検出するコードを追加できません。
つまり、小人が動いたときに小人と弾が重なるかどうかは検出できないが、弾が動いたときに小人と弾が重なるかどうかは検出できます! では、作成後の各弾にmove.after関数を設定します。

setInterval(function () {
    let bullet = mygame.create.air(28, 1)
    bullet.style = pattern.bullet
    bullet.automove.left(29, 4)
    // この部分が重要👇👇
    bullet.move.after = function () {
        if (human.isoverlapping(bullet)) {
            mygame.over.lose()
        }
    }
    // この部分が重要👆👆
}, 1000)

こうすると、これらの危険物の致死性はすべて設定されました。
この項の最後に、星の扱いについて簡単に説明します。
星に対する設定は、小人が星を食べる、つまり小人と重なると星が消えます。小人が右下(star1)と左上(star2)の星を食べるとジャンプの高さが上がり、右上(star3)の星を食べるとゲームの勝ちになるということです。
亀甲の重なり検出と同様に,星の重なり検出も human.move.after 関数に付加しています。

if (human.isoverlapping(star1)) {
    human.strength = 5
    star1.remove()
}

human.strengthは、小人のジャンプの最大高さを制御します。デフォルト値は3。小人が二段目の台までジャンプできるように、より高い高さを選んでいます。removeアクションはブロックを自分から取り除くために使用され、star1.remove()が実行されると星は消えます。
最後の二つの星の扱いはお任せします。 行き詰まったときは、example.htmlをメモ帳で開く、ヒントが表示されますよ。

Discussion