JSでシューティングゲームを作ったのでメモ。
こちらのサイトを参考にして作りました。ほとんど、こちらのサイトのコードですが、今のところ一箇所のみ(敵キャラの動きを一つ追加した)自分で追加しました。今後、さらに改造を加える予定です。
とあるので、この記事に自分が手打ちしたコードを置いておきます。手打ちしたので、コメントやコードの位置を自分で分かるように編集しています。
参考サイトに細かい解説はあるので、この記事では解説しません。自分と同じように参考サイトを見て、作ってみたい方は途中から変数の宣言の説明の省略等があるので、注意してください。自分はそこでハマりました。
このプログラムを通していろいろ学びました。
例えばこれ。
switch(true){
case i = 0:
break;
case j = 0:
break;
}
switchを最初からtrueにし、caseで条件分けしている。こうする事でより柔軟に場合分けできます。
他には、今まである変数がtrueかfalseで場合分けするとき以下のように書いていました。
if(whichType === true){
//処理
}else{
//処理
}
しかし先ほどのswitchのコードと似ていますが、このようにする事で簡略化できます。
if(whichType){
//処理
}else{
//処理
}
今後はこのように書くように統一したいと思います。
プログラムを書いている中で、自分の数学の知識が足りていないことを痛感させられました。例えば、アイキャッチ画像(最初の画像)のBossの動きです。これは理解出来ていないので、sin cos の復習を今後したいと思います。↓
以下コードです。
index.html
<!DOCTYPE html>
<html>
<head>
<scriptsrc=”common.js”></script>
<scriptsrc=”main.js”></script>
<scriptsrc=”character.js”></script>
<scriptsrc=”boss.js”></script>
<style>
canvas {
border: 1pxsolidgray;
}
</style>
</head>
<body>
<canvasid=”screen”></canvas>
<pid=”info”></p>
</body>
</html>
**main.js**
//global--------------------------------------------------
var screenCanvas, info;
var run = true;
var fps = 1000 / 30;
var mouse = new Point;
var ctx; //canvas2d コンテキスト格納用
var fire = false;
var counter = 0;
var score = 0;
var message = '';
// const--------------------------------------------------
var CHARA_COLOR = 'rgba(0, 0, 255, 0.75)';
var CHARA_SHOT_COLOR = 'rgba(0, 255, 0, 0.75)';
var CHARA_SHOT_MAX_COUNT = 10;
var ENEMY_COLOR = 'rgba(255, 0, 0, 0.75)';
var ENEMY_MAX_COUNT = 10;
var ENEMY_SHOT_COLOR = 'rgba(255, 0, 255, 0.75)';
var ENEMY_SHOT_MAX_COUNT = 100;
var BOSS_COLOR = 'rgba(128, 128, 128, 0.75)';
var BOSS_BIT_COLOR = 'rgba(64, 64, 64, 0.75)';
var BOSS_BIT_COUNT = 5;
//main----------------------------------------------------
window.onload = function () {
//何にかの変数
var i, j;
var p = new Point();
//スクリーンの初期化
screenCanvas = document.getElementById('screen');
screenCanvas.width = 256;
screenCanvas.height = 256;
//自機の位置(マウスの位置)を真ん中あたりに設定
mouse.x = screenCanvas.width / 2;
mouse.y = screenCanvas.height - 20;
//2dコンテキスト
ctx = screenCanvas.getContext('2d');
//イベントの登録
screenCanvas.addEventListener('mousemove', mouseMove, true);
screenCanvas.addEventListener('mousedown', mouseDown, true);
window.addEventListener('keydown', keyDown, true);
//その他のエレメント関連
info = document.getElementById('info');
//自動初期化
var chara = new Character();
chara.init(10);
var charaShot = new Array(CHARA_SHOT_MAX_COUNT);
for (i = 0; i < CHARA_SHOT_MAX_COUNT; i++) {
charaShot[i] = new CharacterShot()
}
var enemy = new Array(ENEMY_MAX_COUNT);
for (i = 0; i < ENEMY_MAX_COUNT; i++) {
enemy[i] = new Enemy();
}
var enemyShot = new Array(ENEMY_SHOT_MAX_COUNT);
for (i = 0; i < ENEMY_SHOT_MAX_COUNT; i++) {
enemyShot[i] = new EnemyShot();
}
var boss = new Boss();
var bit = new Array(BOSS_BIT_COUNT);
for (i = 0; i < BOSS_BIT_COUNT; i++) {
bit[i] = new Bit();
}
//レンダリング処理を呼び出す
(function () {
//カウンター追加
counter++;
//HTMLを更新
info.innerHTML = mouse.x + ':' + mouse.y;
//screenをクリア
ctx.clearRect(0, 0, screenCanvas.width, screenCanvas.height);
//自機------------------------------
//パスの設定を開始
ctx.beginPath();
//自機の位置を設定
chara.position.x = mouse.x;
chara.position.y = mouse.y;
//自機を描くパスを設定
ctx.arc(chara.position.x, chara.position.y, chara.size, 0, Math.PI * 2, false);
//自機の色を設定する
ctx.fillStyle = CHARA_COLOR;
//自機を描く
ctx.fill();
//fireフラグの値により分岐
if (fire) {
//全ての自機ショットを調査する
for (i = 0; i < CHARA_SHOT_MAX_COUNT; i++) {
//自機ショットが既に発射されているかチェック
if (!charaShot[i].alive) {
//自機ショットを新規にセット
charaShot[i].set(chara.position, 3, 5);
//ループを抜ける
break;
}
}
//フラグを降ろしておく
fire = false;
}
//自機の動きとショット------------------------------------------
//パスの設定を開始
ctx.beginPath();
//全ての自機ショットを調査する
for (i = 0; i < CHARA_SHOT_MAX_COUNT; i++) {
//自機ショットが既に発射されているかチェック
if (charaShot[i].alive) {
//自機ショットを動かす
charaShot[i].move();
//自機ショットを描くパスを設定
ctx.arc(
charaShot[i].position.x,
charaShot[i].position.y,
charaShot[i].size,
0, Math.PI * 2, false
);
//パスをいったん閉じる
ctx.closePath();
}
}
//自機ショットの色を設定する
ctx.fillStyle = CHARA_SHOT_COLOR;
//自機ショットを描く
ctx.fill();
//カウンターの値によってシーンを分岐
switch (true) {
//カウンターが70より小さい
case counter < 70:
message = 'READY...';
break;
//カウンターが100より小さい
case counter < 100:
message = 'GO!!';
break;
//カウンターが100以上
default:
message = '';
//以下ゲーム処理
//エネミーの出現管理-----------------------------------
//100フレームに一度出現させる
if (counter % 100 === 0 && counter < 1000) {
//全てのエネミーを調査する
for (i = 0; i < ENEMY_MAX_COUNT; i++) {
//エネミーの生存フラグをチェック
if (!enemy[i].alive) {
//サイズを決定
var enemySize = 15;
if (counter < 500) {
//タイプを決定するパラメータを算出
j = (counter % 200) / 100;
//タイプに応じて初期位置を決める(type 0 or 1)
p.x = -enemySize + (screenCanvas.width + enemySize * 2) * j
p.y = screenCanvas.height / 2;
//エネミーを新規にセット
enemy[i].set(p, enemySize, j);
} else {
//タイプ2の初期位置を決める
p.x = screenCanvas.width / 2;
p.y = -enemySize * 2;
//タイプ3エネミーを新規にセット
enemy[i].set(p, enemySize, 2);
}
//一体出現させたのでループを抜ける
break;
}
}
}else if (counter === 1000) {
//1000フレーム目にボスを出現させる
p.x = screenCanvas.width / 2;
p.y = -80;
boss.set(p, 50, 30);
//同時にビットも出現させる
for (i = 0; i < BOSS_BIT_COUNT; i++) {
j = 360 / BOSS_BIT_COUNT;
bit[i].set(boss, 15, 5, i * j);
}
}
//エネミー動きとショット(描画はなし)---------------------------------
//パスの設定を開始
ctx.beginPath();
//全てのエネミーを調査する
for (i = 0; i < ENEMY_MAX_COUNT; i++) {
//エネミーが既に発射されているかチェック
if (enemy[i].alive) {
//エネミーを動かす
enemy[i].move();
//エネミーを描くパスを設定
ctx.arc(
enemy[i].position.x,
enemy[i].position.y,
enemy[i].size,
0, Math.PI * 2, false
);
//ショットを打つかどうかパラメータの値からチェック
if (enemy[i].param % 30 === 0) {
//エネミーショットを調査する
for (j = 0; j < ENEMY_SHOT_MAX_COUNT; j++) {
if (!enemyShot[j].alive) {
//エネミーショットを新規にセットする
p = enemy[i].position.distance(chara.position);
p.normalize();
enemyShot[j].set(enemy[i].position, p, 5, 5);
//1個出現させたのでループを抜ける
break;
}
}
}
//パスをいったん閉じる
ctx.closePath();
}
}
//エネミーの色を設定する
ctx.fillStyle = ENEMY_COLOR;
//エネミーを描く
ctx.fill();
//エネミーショットの描画-----------------------------------
//パスの設定
ctx.beginPath();
// 全てのエネミーショットを調査
for (i = 0; i < ENEMY_SHOT_MAX_COUNT; i++) {
// エネミーショットがあるか確認
if (enemyShot[i].alive) {
// エネミーショットを動かす
enemyShot[i].move();
//ショットの設定
ctx.arc(
enemyShot[i].position.x,
enemyShot[i].position.y,
enemyShot[i].size,
0, Math.PI * 2, false
);
// 一旦パスを閉じる
ctx.closePath();
}
}
//ショットの色を指定
ctx.fillStyle = ENEMY_SHOT_COLOR;
//描画
ctx.fill();
//ボス----------------------------------------------
//パスの設定を開始
ctx.beginPath();
//ボスの出現フラグをチェック
if (boss.alive) {
//ボスを動かす
boss.move();
//ボスを描くパスを設定
ctx.arc(
boss.position.x,
boss.position.y,
boss.size,
0, Math.PI * 2, false
);
//パスを一旦閉じる
ctx.closePath();
}
//ボスの色を設定する
ctx.fillStyle = BOSS_COLOR;
//ボスを描く
ctx.fill();
//ビット--------------------------------------------
//パスの設定を開始
ctx.beginPath();
//全てのビットを調査する
for (i = 0; i < BOSS_BIT_COUNT; i++) {
//ビットの出現フラグをチェック
if (bit[i].alive) {
//ビットを動かす
bit[i].move();
//ビットを描くパスを設定
ctx.arc(
bit[i].position.x,
bit[i].position.y,
bit[i].size,
0, Math.PI * 2, false
);
//ショットを打つかどうかパラメータをチェック
if (bit[i].param % 25 === 0) {
//エネミーショットを調査する
for (j = 0; j < ENEMY_SHOT_MAX_COUNT; j++) {
if (!enemyShot[j].alive) {
//エネミーショットを新規にセットする
p = bit[i].position.distance(chara.position);
p.normalize();
enemyShot[j].set(bit[i].position, p, 4, 1.5);
//一個出現させたのでループを抜ける
break;
}
}
}
//パスを一旦閉じる
ctx.closePath();
}
}
//ビットの色を設定する
ctx.fillStyle = BOSS_BIT_COLOR;
//ビットを描く
ctx.fill();
//衝突判定------------------------------------------
//エネミーと自機ショットの衝突判定
//全ての自機ショットを調査する
for (i = 0; i < CHARA_SHOT_MAX_COUNT; i++) {
//自機ショットの生存フラグをチェック
if (charaShot[i].alive) {
//自機ショットとエネミーとの衝突判定
for (j = 0; j < ENEMY_MAX_COUNT; j++) {
//エネミーの生存フラグをチェック
if (enemy[j].alive) {
//エネミーと自機ショットとの距離を計測
p = enemy[j].position.distance(charaShot[i].position);
//衝突判定
if (p.length() < enemy[j].size) {
//生存フラグを降ろす
charaShot[i].alive = false;
enemy[j].alive = false;
//スコアを加算
score++;
//衝突があったのでループを抜ける
break;
}
}
}
//自機ショットとボスビットとの衝突判定
for (j = 0; j < BOSS_BIT_COUNT; j++) {
//ビットの生存フラグをチェック
if (bit[j].alive) {
//ビットと自機ショットとの距離を計測
p = bit[j].position.distance(charaShot[i].position);
//衝突判定
if (p.length() < bit[j].size) {
//耐久値をデクリメントする
bit[j].life--;
//自機ショットの生存フラグを降ろす
charaShot[i].alive = false;
//耐久値がマイナスになったらビットの生存フラグを降ろす
if (bit[j].life < 0) {
bit[j].alive = false;
score += 3;
}
//衝突があったのでループを抜ける
break;
}
}
}
//ボスの生存フラグをチェック
if (boss.alive) {
//自機ショットとボスの衝突判定
p = boss.position.distance(charaShot[i].position);
if (p.length() < boss.size) {
//耐久値をデクリメント
boss.life--;
//自機ショットの生存フラグを降ろす
charaShot[i].alive = false;
//耐久値がマイナスになったらクリア
if (boss.life < 0) {
score += 10;
run = false;
message = 'CLEAR!!';
}
}
}
}
}
//自機とエネミーショットの衝突判定
//全てのエネミーショットを調査する
for (i = 0; i < ENEMY_SHOT_MAX_COUNT; i++) {
//エネミーショットの生存フラグをチェック
if (enemyShot[i].alive) {
//自機とエネミーショットとの距離を計測
p = chara.position.distance(enemyShot[i].position);
//衝突判定
if (p.length() < chara.size) {
//自機の生存フラグを降ろす
chara.alive = false;
//衝突があったのでパラメータを変更してループを抜ける
run = false;
message = 'GAME OVER!!';
break;
}
}
}
break;
}
//スコア表示更新
info.innerHTML = 'SCORE: ' + (score * 100) + ' ' + message;
//フラグにより再帰呼び出し
if (run) { setTimeout(arguments.callee, fps); }
})();
};
//event---------------------------------------------------
function mouseMove(event) {
//マウスカーソル座標の更新
mouse.x = event.clientX - screenCanvas.offsetLeft;
mouse.y = event.clientY - screenCanvas.offsetTop;
}
function mouseDown(event) {
//フラグを立てる
fire = true;
}
function keyDown(event) {
//キーコードを取得
var ck = event.keyCode;
//Escキーが押されていたらフラグを降ろす
if (ck === 27) { run = false; }
}
common.js
function Point(){
this.x = 0;
this.y = 0;
}
Point.prototype.distance = function(p){
var q = new Point();
q.x = p.x - this.x;
q.y = p.y - this.y;
return q;
};
Point.prototype.length = function(){
return Math.sqrt(this.x * this.x + this.y * this.y);
};
Point.prototype.normalize = function(){
var i = this.length();
if(i>0){
var j = 1 / i;
this.x *= j;
this.y *= j;
}
};
character.js
function Character() {
this.position = new Point();
this.size = 0;
}
Character.prototype.init = function (size) {
this.size = size;
}
function CharacterShot() {
this.position = new Point();
this.size = 0;
this.speed = 0;
this.alive = false;
}
CharacterShot.prototype.set = function (p, size, speed) {
//座標をセット
this.position.x = p.x;
this.position.y = p.y;
//サイズ、スピードをセット
this.size = size;
this.speed = speed;
//生存フラグを立てる
this.alive = true;
};
CharacterShot.prototype.move = function () {
//座標を真上にspeed分だけ移動させる
this.position.y -= this.speed;
//一定以上の座標に到達していたら生存フラグを降ろす
if (this.position.y < - this.size) { this.alive = false; } } function Enemy() { this.position = new Point(); this.size = 0; this.type = 0; this.param = 0; this.alive = false; } Enemy.prototype.set = function (p, size, type) { //座標をセット this.position.x = p.x; this.position.y = p.y; //サイズ、タイプをセット this.size = size; this.type = type; //パラメータをリセット this.param = 0; //生存フラグを立てる this.alive = true; }; Enemy.prototype.move = function () { //パラメータをインクリメント this.param++; //タイプに応じて分岐 switch(this.type) { case 0: //x方向へまっすぐ進む this.position.x += 2; //スクリーンの右端より奥に到達したら生存フラグを降ろす if (this.position.x > this.size + screenCanvas.width) {
this.alive = false;
}
break;
case 1:
//マイナスx方向へまっすぐ進む
this.position.x -= 2;
//スクリーンの左端より奥に到達したら生存フラグを降ろす
if (this.position.x < -this.size) { this.alive = false; } break; case 2: //画面下(yの+方向)にまっすぐ進む this.position.y +=2; //スクリーン下に到達したら生存フラグを降ろす if(this.position.y > this.size + screenCanvas.height){
this.alive = false;
}
break;
}
};
function EnemyShot(){
this.position = new Point();
this.vector = new Point();
this.size = 0;
this.speed = 0;
this.alive = false;
}
EnemyShot.prototype.set = function(p,vector,size,speed){
//座標、ベクトルをセット
this.position.x = p.x;
this.position.y = p.y;
this.vector.x = vector.x;
this.vector.y = vector.y;
//サイズ、スペードをセット
this.size = size;
this.speed = speed;
//生存フラグを立てる
this.alive = true;
};
EnemyShot.prototype.move = function(){
//座標をベクトルに応じてspeed分だけ移動させる
this.position.x += this.vector.x * this.speed;
this.position.y += this.vector.y * this.speed;
//一定以上の座標に到達していたら生存フラグを降ろす
if(
this.position.x < -this.size ||
this.position.y < -this.size || this.position.x > this.size + screenCanvas.width ||
this.position.y > this.size + screenCanvas.height
){
this.alive = false;
}
}
boss.js
//boss-----------------------------------------
function Boss() {
this.position = new Point();
this.size = 0;
this.life = 0;
this.param = 0;
this.alive = false;
}
Boss.prototype.set = function (p, size, life) {
//座標をセット
this.position.x = p.x;
this.position.y = p.y;
//サイズ、耐久値をセット
this.size = size;
this.life = life;
//パラメータをリセット
this.param = 0;
//生存フラグを立てる
this.alive = true;
};
Boss.prototype.move = function () {
var i, j;
//パラメータをインクリメント
this.param++;
//パラメータに応じて分岐
switch (true) {
case this.param < 100:
//下方向にまっすぐ進む
this.position.y += 1.5;
break;
default:
//パラメータからラジアンを求める
i = ((this.param - 100) % 360) * Math.PI / 180;
//ラジアンから横移動量を算出→サイン、コサイン勉強必要!
j = screenCanvas.width / 2;
this.position.x = j + Math.sin(i) * j;
break;
}
}
//boss bit-------------------------------------------------------
function Bit(){
this.position = new Point();
this.parent = null;
this.size = 0;
this.life = 0;
this.param = 0;
this.alive = false;
}
Bit.prototype.set = function(parent,size,life,param){
//母体となるボスをセット
this.parent = parent;
//サイズ、耐久値をセット
this.size = size;
this.life = life;
//パラメータに初期値をセット
this.param = param;
//生存フラグを立てる
this.alive = true;
};
Bit.prototype.move = function(){
var i,x,y;
//パラメータをインクリメント
this.param++;
//パラメータからラジアンを求める
i = (this.param % 360) * Math.PI / 180;
//ラジアンから横移動量を算出
x = Math.cos(i) * (this.parent.size + this.size);
y = Math.sin(i) * (this.parent.size + this.size);
this.position.x = this.parent.position.x + x;
this.position.y = this.parent.position.y + y;
}