- Javascript シューティングゲーム
- 目次
- この教材について
- Step 00: CodeSandboxアカウントの登録
- Step 01: Java Script と PIXI.jsの始まり
- Step 02: キャラクターを表示してみよう
- Step 03: Java Scriptについて
- Step 04: 自機を表示しよう
- Step 05: 矢印キーで自機を移動しよう
- Step 06: 自機の移動範囲を制限して画面から出ないようにしよう
- Step 07: ミサイルを発射しよう
- Step 08: エイリアン(敵)を配置しよう
- Step 09: エイリアンを規則的に動かそう
- Step 10: エイリアンもミサイルを打ってくるようにしよう
- Step 11: ゲームクリア/ゲームオーバー
- Step 12: ミサイルを画面内3発まで発射できるようにしよう
- Step 13: Zキーで3-wayミサイルを発射できるようにしよう
- Step 14: Xキーでレーザーを発射できるようにしよう
- さらに改良するには?
- これは「シューティングゲーム」の作り方を通してJava ScriptとPIXI.jsを学ぶための教材です
- この教材ではこのCodeSandboxの教材を使用します
- 初回はCodeSandboxにアカウントがないと思いますので、以下の手順でアカウントを作成しましょう
- https://codesandbox.io/から
Sign In
を選択し、Sign in with Google
を選択 - 自分のGoogle Account用のパスワードを入力すると、
username
とdisplay name
を聞かれます - この名前は外部から見られますので、本名や本人が特定できる情報は入力しないでください
- 入力したら、
Finish Sign Up
ボタンを押すと登録完了です
- https://codesandbox.io/から
- 次回以降は今回作成したアカウントで
Sign In
(ログイン)できます
- まずはCodeSandboxの教材を開いてみましょう
- Filesに進むと、以下のようなファイルが用意されています
src
フォルダ内にあるindex.js
を開いて、内容を全て以下のスクリプトで置き換えてください。同様の内容はCode Sandbox内のstep01.js
にも保存されています。- Ctrl-Sでセーブすると、初回はForkするか聞かれますので、Yesと答えてください。
- Forkすると、初めに読み込み専用で開いたCode Sandoboxの教材スクリプトを全て自分の環境にコピーし、変更・セーブできるようになります。
import "./styles.css";
import { init } from "./util.js";
// 初期化
init(setUp, gameLoop);
function setUp() {}
function gameLoop() {}
index.js
の中をstep02.js
で置き換えて、alienを表示してみよう- Step 01からの変更点は、2行目の
import
とsetUp
関数の中身です import
は今回は気にしないでくださいsetUp
はプログラムを実行すると最初に1回だけ呼び出される関数ですgameLoop
はゲーム開始後setUp
が呼ばれた後に、1秒間に60回定期的に呼ばれる関数です//
以降はコメント行で、自由なコメント(自分のメモ)を記述できます
import "./styles.css";
import { init, sprites, createSprite } from "./util.js";
// 初期化
init(setUp, gameLoop);
function setUp() {
let alien = createSprite(sprites.alien1);
alien.position.x = 16;
alien.position.y = 16;
}
function gameLoop() {}
- うまく行くとalien1が表示されます
- Spriteを回転させてみよう
- ヒント:
alien.rotation
- ヒント:
- Spriteの位置を変えてみよう
- ヒント:
alien.position.x
- ヒント:
-
Step 03では、Step 04以降ゲームを作っていく上で必要なJavascriptの基本について学びます
-
index.js
のsetUp
関数の中を全て消去し、以下のようにconsole.log
を追加して実行してみよう// Step 03 console.log("hello");
-
Console
ウィンドウを開くとhello
と表示されたのがわかります
-
変数とは
-
変数
x
の作成と代入 -
x = 3
の=
は、右にある数字の3を左にある変数x
に「代入する(設定する)」という意味ですlet x = 3; console.log(x);
-
作成した変数
x
に1を足す -
x = 3
の=
は、右にある数字の3を左にある変数x
に「代入する(設定する)」という意味ですx = x + 1; console.log(x);
-
変数同士は演算することもできます
let a = 10; let b = 2; let c = a + b; console.log(c);
-
整数だけでなく、少数や文字列も変数に代入できます
let d = 123.45; let s = "hello"; console.log(d); console.log(s);
- 文字列と少数又は整数を
+
すると、文字列として連結されます
console.log(d+s);
-
-
if
構文とは?let x1 = 3; let x2 = 4; /// === では、左辺と右辺が同じならTrueになります if (x1 === 3) { console.log("x1 is 3"); } else { console.log("x1 is not 3"); } if (x2 === 3) { console.log("x2 is 3"); } else { console.log("x2 is not 3"); }
-
==
(型が違っても同じ値ならTrue)と===
(型も同じでなくてはならない)let x3 = 3; let s3 = "3"; if (x3 == s3) { console.log("=="); } else { console.log("not =="); } if (x3 === s3) { console.log("==="); } else { console.log("not ==="); }
-
for
構文とは?for
基本型
for (let x4 = 1; x4 <= 3; x4 = x4 + 1) { console.log("x4=" + x4); }
++
のようにも書けます
for (let x4 = 1; x4 <= 3; x4++) { console.log("x4=" + x4); }
-
array
(配列)とはなんだろう?array
を使って複数の整数を格納してみよう- index(インデックス)を使ってarrayの中身を表示してみよう
for
を使ってarrayの中身を全て表示してみよう- index(インデックス)を使ってarrayの中身を変更してみよう
let a1 = [3, 5, 9]; console.log(a1[0]); console.log(a1[1]); console.log(a1[2]); for (let i = 0; i <= 2; i++) { console.log("a1[" + i + "]=" + a1[i]); } a1[1] = 100; for (let i = 0; i <= 2; i++) { console.log("a1[" + i + "]=" + a1[i]); }
-
dictionary
(辞書とはなんだろう)?dictionary
を使って複数の文字列を格納してみよう- key(キー)を使ってdictionaryの中身を表示してみよう
- key(キー)を使ってdictionaryの中身を変更してみよう
for
を使ってdictionaryの中身を全て表示してみよう
let d1 = { apple: 3, orange: 5 }; console.log(d1); console.log(d1["apple"]); console.log(d1["orange"]); d1["apple"] = 100; console.log(d1["apple"]); for (let [k, v] of Object.entries(d1)) { console.log("k="+k+", v="+v); }
-
関数とは
myadd(a, b)
関数を作って、a+bを返そう
// setUp内にこれを書く let f1 = 10; let f2 = 20; let f3 = myadd(f1, f2); console.log(f3); // 一番外のスコープにこれを書く function myadd(a, b) { return a + b; }
-
a1
という変数名のarrayを作って、for
を使ってそのarrayに1から100までの整数を格納してみようa1 = [] for(let i=0;i<100;i++){ a1[i] = i; }
-
1から100まで格納した上記
a1
を列挙(巡回)して、全ての数字を表示してみよう -
dictionary
にいくつかのkey - valueペアを代入しよう -
その
dictionary
を列挙(巡回)して、全てのkey - valueペアをconsole.log
してみよう -
myadd
のように、与えられた2つの整数を引き算して値を返すmysub(a, b)
関数を作ろう
- Step 03までで準備は完了です!ここからはゲームを作っていきましょう
- まずは、
step04.js
の内容をindex.js
にコピーし、実行してみよう init(setUp, gameLoop);
を最初に1回だけ読んで、ゲームを初期化しますsetUp
は最初に1回だけ呼ばれますgameLoop
は1秒間に60回呼ばれますgameLoop
の中でキーボードの状態を取得し、ゲームの進行を行います
player.png
を自機として表示してみようsetUp
内で自機を作成してみよう- 事前に
sprites.player
,sprites.alien1
,sprites.missile
などが用意されています - 何が用意されているかは
util.js
の中のinit
関数を見てみよう util.js
に用意されているcreateSprite
関数にsprite.*を渡すと、新しくそのスプライトが作成されます
function setUp() {
// 自機のセットアップ
let player = createSprite(sprites.player);
player.x = 160;
player.y = 220;
}
- 自機の初期表示位置は(160,220)にしよう
player.x
やplayer.y
を変更して、何が変わるのか見てみよう
- Step 05ではStep 04で表示した自機をキーボードで動かします
- キーボード入力は
util.js
内のkeyboard
を使用して取得します - まず、グローバル変数の定義のところで
left
とright
を定義して、それぞれ左・右矢印キーに割り当てます - 1秒に60回呼ばれる
gameLoop
にて、- キーボードの左矢印キーが押されたら
vx=-4
、右矢印キーが押されたらvx=4
、それ以外はvx=0
とします - プレイヤーの横座標を示す
player.x
にvx
を足します vx
はx方向のvelocity(加速度)という意味です
- キーボードの左矢印キーが押されたら
player
はsetUp
関数で初期化されますが、gameLoop
関数でも使えなくてはなりません。そのため、let player;
を一番外側において、どの関数でも使えるグローバル変数にします
import "./styles.css";
import { init, keyboard, sprites, createSprite } from "./util.js";
// グローバル変数
let player;
let left = keyboard("ArrowLeft");
let right = keyboard("ArrowRight");
// 初期化
init(setUp, gameLoop);
function setUp() {
// 自機のセットアップ
player = createSprite(sprites.player);
player.vx = 0;
player.x = 160;
player.y = 220;
}
function gameLoop() {
// 自機を動かす
let vx = 0;
if (left.isDown) {
vx = -4;
} else if (right.isDown) {
vx = 4;
}
player.x += vx;
}
- 実行してみましょう
-
自機が画面から出ないように、以下の処理を追加しよう
- 自機が画面の左端よりも左に来たら、左端に戻す
- 自機が画面の右端よりも右に来たら、右端に戻す
-
画面のサイズは、
app
をimportすることにより取得できますimport { app, init, keyboard, sprites, createSprite } from "./util.js"; console.log(app.view.width); // 320です
-
上記を行うには、
gameLoop
内に以下のように書くことができますMath.max
を使って自機のX座標を現在の値と画面の左端のうち大きい方にするMath.min
を使って自機のX座標を現在の値と画面の右端のうち小さい方にする
-
境界チェックのためのこの
Math.max/min
の使い方はよくでてきますので覚えておきましょうplayer.x = Math.max(player.x, player.width / 2); player.x = Math.min(player.x, app.view.width - sprites.player.width / 2);
- ここまでで自機が移動できるようになりましたので、次にスペースキーでミサイルを1発だけ発射できるようにしよう
- ミサイルが画面にないときはまたミサイルが発射でき、ミサイルが画面内にあるときは発射できないようにしよう
- ミサイルが発射されたら
missile
の位置を自機の位置あたりに設定し、gameLoop
内で位置を更新しよう - ミサイルは画面の外に出たら消えるようにしよう
- まず、ミサイルを保存するグローバル変数
missile
を作成し、null
に初期化します。null
は何もない値という意味です
let missile = null;
- そして、
setUp
にスペースキーの処理を追加します。仕組みはplayer
の動かし方と同じです - 今回、ミサイル発射時の処理は
player
の時よりちょっと長いため、fire
という関数にします
// グローバル変数
let space = keyboard(" ");
function gameLoop() {
...
if (space.isDown) {
console.log("fire!");
fire();
}
...
fire
関数で、missile
がnullの時だけミサイルを作成し、初期位置を設定します。こうすることにより、ミサイルが何発も同時に発射されることを防ぎます
function fire() {
if (missile === null) {
missile = createSprite(sprites.missile);
missile.x = player.x;
missile.y = player.y - player.height / 2;
missile.vy = -4;
missile.vx = 0;
}
}
- 最後に、
gamelooop
でミサイルを動かします - また、ミサイルが画面外に出たらミサイルを
removeSripte
で削除し、nullに初期化します
import {
// ... 省略
removeSprite
} from "./util.js";
function gameLoop() {
// ... 省略
// ミサイルを動かす
if (missile !== null) {
missile.y += missile.vy;
missile.x += missile.vx;
// ミサイルが画面外に出たら削除し、nullに初期化
if (missile.y < 0) {
removeSprite(missile);
missile = null;
}
}
}
- 詳細は
step07.js
を参照してください
- 配列にエイリアンを格納し、表示しよう
- まず、複数のエイリアンを格納する
alians
というarray(配列)を作成します
let aliens = [];
- そして、
setUp
関数内で3行5列=15匹のエイリアンのSpriteを作成し、作ったSpriteをalians
のarrayにpush(追加)します - こうすることにより、
aliens
は15個の要素を持つ配列になります
// エイリアンのセットアップ
for (let j = 0; j < 3; j++) {
for (let i = 0; i < 5; i++) {
let alien;
if (j === 0) {
alien = createSprite(sprites.alien1);
} else if (j === 1) {
alien = createSprite(sprites.alien2);
} else {
alien = createSprite(sprites.alien3);
}
alien.x = 16 + i * 64;
alien.y = 20 + j * 32;
aliens.push(alien);
}
}
- ミサイルが当たったらエイリアンを消去しよう
- ミサイルにあったかどうかの判定はミサイルとエイリアンのx, y座標の差の絶対値が16以下の場合としよう
- ミサイルとエイリアンの当たり判定を
gameLoop
内で行います
function gameLoop() {
// ... 省略
// hit test
hitTest();
}
function hitTest() {
// ミサイルとエイリアンのあたり判定
for (let i = 0; i < aliens.length; i++) {
let alien = aliens[i];
if (
missile !== null &&
Math.abs(missile.x - alien.x) < 16 &&
Math.abs(missile.y - alien.y) < 16
) {
removeSprite(alien);
// spliceは, 第1引数で指定したi番目の要素から第1引数で指定した1個の要素を取り除くという意味
// aliens配列は、この操作の後aliens[0], aliens[1], ..., aliens[i-1], aliens[i+1], ...
// となる
// つまり、i番目の要素が取り除かれる
aliens.splice(i, 1);
removeSprite(missile);
missile = null;
}
}
}
- ここまでで少しゲームとして遊べるようになります
- ここではエイリアンを右に1ドット x 30回、左に1ドット x 30回、また右に1ドット x 30回、...というように周期的に動かしましょう
- まず、何回描画されたのかをカウントするためのグローバル変数
frame
を追加し、gameLoop
が呼ばれるたびに1ずつ増加させます(インクリメントと言います)。 - そして、
hitTest
の前にmoveAliens
を呼び出し、すべてのエイリアンを動かします
let frame = 0;
function gameLoop() {
frame++;
// ...省略
// エイリアンを動かす
moveAliens();
// hit test
hitTest();
}
moveAliens
はframe
を使用して現在何回描画されているのかを確認し、それによってエイリアンを右または左に動かします%
は剰余を示す演算子で、frame % 60
はfrmame
を60で割った時の余りを示します
function moveAliens() {
for (let i = 0; i < aliens.length; i++) {
let alien = aliens[i];
if (frame % 60 < 30) {
alien.x += 1;
} else {
alien.x -= 1;
}
}
}
- エイリアンもミサイルを打ってくるようにしよう
- すでに3発のミサイルがエイリアンから画面内に発射されていたらそれ以上は発射しないようにしよう
- 上記の実装のために、まずエイリアンのミサイルを保持するグローバル変数を定義し、カラに初期化しよう
let alienMissiles = [];
- そして、ランダムに最大3発までエイリアンが撃ってくるよにしよう
Math.random()
は0から1までの少数をランダムに返す関数です- ここでは0.7%の確率で、かつまだミサイルが3発撃たれていなかったらエイリアンがミサイルを撃つようにしています
function gameLoop() {
// ...省略
// エイリアンのミサイル発射
alienFire();
// 省略...
}
function alienFire() {
for (let i = 0; i < aliens.length; i++) {
let alien = aliens[i];
if (alienMissiles.length >= 3) return;
if (Math.random() < 0.007) {
let alienMissile = createSprite(sprites.alienMissile);
alienMissile.x = alien.x;
alienMissile.y = alien.y + alien.height / 2;
alienMissiles.push(alienMissile);
}
}
}
- 上記だけだと、ミサイルが発射された後動かないので、動かしましょう
- もしミサイルが画面外に出たらさくじょしよう。
alienMissiles
を巡回中にその要素を削除するため、該当ミサイルが削除対象の場合、Step 09同様splice
で項目を削除し、1つずれたalienMissile
に対処するためi
を-1して再度for
ループさせます
function gameLoop() {
// ...省略
// エイリアンのミサイルを動かす
for (let i = 0; i < alienMissiles.length; i++) {
let alienMissile = alienMissiles[i];
alienMissile.y += 2;
if (alienMissile.y > app.view.height + alienMissile.height / 2) {
removeSprite(alienMissile);
alienMissiles.splice(i, 1);
i -= 1;
}
}
// 省略...
- この段階ではエイリアンのミサイルに当たっても何も起こりません。次のステップで対応します
- エイリアンのミサイルに自機が当たったら
Game Over
と表示しよう - まず、このために
showMessage
関数がutil.js内に用意されていますので、importします - 同様に
hideMessage
もimportします
import {
// ...省略
showMessage,
hideMessage
} from "./util.js";
- エイリアンのミサイルに当たった場合には
gameover
グローバル変数をtrueにしてshowMessage
を呼び出します - この
gameover
変数は後ほど使います - 同様に、すぐに後で使うので、
gameclear
とcounter
も追加します
let counter = 0;
let gameover = false;
let gameclear = false;
function hitTest() {
// ... 省略
// エイリアンのミサイルと自機の当たり判定
for (let i = 0; i < alienMissiles.length; i++) {
let alienMissile = alienMissiles[i];
if (
Math.abs(alienMissile.x - player.x) < 16 &&
Math.abs(alienMissile.y - player.y) < 16
) {
gameover = true;
showMessage("Game Over");
}
}
- エイリアンを全て倒したら
Clear
と表示しよう
function hitTest() {
// ...省略
// 残りエイリアンのカウント
if (aliens.length === 0) {
gameclear = true;
showMessage("Clear");
return;
}
// 省略...
- ゲームクリアもしくはゲームオーバーを3秒表示したら自動的にゲームを再開しよう
- 再度初めからゲームを開始するために、一部の
setUp
内の処理をresetGame
関数に移動します - また、エイリアンのミサイルとエイリアンの初期化処理(リセット)を追加します
function resetGame() {
// エイリアンのミサイルのリセット
for (let i = 0; i < alienMissiles.length; i++) {
let alienMissile = alienMissiles[i];
removeSprite(alienMissile);
}
alienMissiles = [];
// エイリアンのリセット
for (let i = 0; i < aliens.length; i++) {
let alien = aliens[i];
removeSprite(alien);
}
aliens = [];
// 自機のセット
player.vx = 0;
player.x = 160;
player.y = 220;
// エイリアンのセットアップ
for (let j = 0; j < 3; j++) {
for (let i = 0; i < 5; i++) {
let alien;
if (j === 0) {
alien = createSprite(sprites.alien1);
} else if (j === 1) {
alien = createSprite(sprites.alien2);
} else {
alien = createSprite(sprites.alien3);
}
alien.x = 16 + i * 64;
alien.y = 20 + j * 32;
aliens.push(alien);
}
}
}
function setUp() {
// ...省略
resetGame();
}
function gameLoop() {
frame++;
if (gameover || gameclear) {
counter++;
if (counter > 180) {
resetGame();
counter = 0;
gameover = false;
gameclear = false;
hideMessage();
}
return;
}
// 省略...
- 配列を使用してミサイルを画面内3発まで発射できるようにしよう
- missile.kindを"missile"にします(Step 14でLaserと区別するために使います)
let missiles = [];
function gameLoop(){
// ...省略
// ミサイルを動かす
for (let i = 0; i < missiles.length; i++) {
let missile = missiles[i];
missile.y += missile.vy;
missile.x += missile.vx;
if (missile.y < -missile.height) {
missiles.splice(i, 1);
removeSprite(missile);
i -= 1;
}
}
// 省略...
function fire() {
if (missiles.length < 3) {
let missile = createSprite(sprites.missile);
missile.x = player.x;
missile.y = player.y - player.height / 2;
missile.vy = -4;
missile.vx = 0;
missile.kind = "missile";
missiles.push(missile);
}
}
function hitTest() {
// ミサイルとエイリアンのあたり判定
for (let i = 0; i < aliens.length; i++) {
for (let j = 0; j < missiles.length; j++) {
let alien = aliens[i];
let missile = missiles[j];
if (
Math.abs(missile.x - alien.x) < 16 &&
Math.abs(missile.y - alien.y) < 16
) {
aliens.splice(i, 1);
removeSprite(alien);
i -= 1;
missiles.splice(j, 1);
removeSprite(missile);
j -= 1;
// このalienは削除されたので、breakしてjのループを抜ける
break;
}
}
}
// 省略...
- ただし、このままでは連続して3発、短い間に発射されてしまうので、20/60秒(0.33秒)間は次のミサイルが発射されないように工夫しよう
// グローバル変数
let readyToFire = 0;
function gameLoop() {
...
if (readyToFire === 0 && space.isDown) {
readyToFire = 20;
console.log("fire!");
fire();
} else {
readyToFire = Math.max(0, readyToFire - 1);
}
...
}
// グローバル変数
let z_key = keyboard("z");
function gameLoop() {
...
if (readyToFire === 0 && space.isDown) {
readyToFire = 20;
console.log("fire!");
fire();
} else if (readyToFire === 0 && z_key.isDown) {
readyToFire = 20;
console.log("3-way!");
fire3way();
} else {
readyToFire = Math.max(0, readyToFire - 1);
}
...
// 省略...
function fire3way() {
if (missiles.length > 0) {
return;
}
for (let i = -2; i <= 2; i += 2) {
let missile = createSprite(sprites.missile);
missile.x = player.x;
missile.y = player.y - player.height / 2;
missile.vy = -4;
missile.vx = i;
missile.kind = "missile";
missiles.push(missile);
}
}
fireLaser
にはmissile.kindを"laser"にします- これによりエイリアンとミサイルの当たり判定の時にLaserはミサイルを消去しないようにできます
function fireLaser() {
if (missiles.length < 3) {
let missile = createSprite(sprites.laser);
missile.x = player.x;
missile.y = player.y - player.height / 2;
missile.vy = -4;
missile.vx = 0;
missile.kind = "laser";
missiles.push(missile);
}
}
function gameLoop() {
...
} else if (readyToFire === 0 && x_key.isDown) {
readyToFire = 20;
console.log("laser!");
fireLaser();
} else {
- そして、missile.kindが"missile"の時だけミサイルを消滅させるようにします
function hitTest() {
// ミサイルとエイリアンのあたり判定
// ... 省略
if (missile.kind === "missile") {
missiles.splice(j, 1);
removeSprite(missile);
j -= 1;
}
- 右上にスコアを表示しましょう
- いきなりゲームが始まるのではなく、タイトル画面をつけるにはどうしたら良いでしょう
- どうやったらエイリアンを回転できるか調べてみましょう
- エイリアンの横の動きをゆらゆらさせるにはどうすればいいでしょう
- どうやったらエイリアンや自機をアニメーションできるか調べてみましょう