Unity触り始めたけど、全然分からんからJavaScriptで修行する ~シューティングゲーム編~
挨拶的なの
お久しぶりですもーふです。最近は寒いですね。名前の通り布団やこたつから出られません。今はこたつに入りながら書いてます。10月後半は前回に引き続いてjavascriptで遊んでました。今回はシューティングゲームを作ったので備忘録かねてまた記事を書いていこうと思います。またゲーム作成にはチャットgptを使用していますので、ご了承ください。
ちなみに前回の記事はこちらです。ブロック崩しゲームを作りました。該当記事のリンクにも張ったブロック崩しゲーム作成のHPを参考にchatGPTを大いに使い作成しました。よかったらこちらもご覧ください。
シューティングゲーム作成の理由
なぜ作成するゲームをシューティングにしたかというと、前回作成したブロック崩しゲームを応用したら作るのが簡単かもと思ったからです。
ブロック崩しゲームでは主にボールの移動、ボールがブロックに当たったらブロックの消失、キー入力でパドルの移動、ライフ表示…など、オブジェクトの移動と当たり判定、UIの表示などを作成しました。(雰囲気で単語を使ってるので単語が間違っているかもしれません)
シューティングゲームを作成しようと考えたときに、概ね敵の移動、弾の当たり判定、キー入力でパドルの移動などを実装すればいいのかと思い、いけそうだったので作成に取り掛かりました。
作成したシューティングゲーム
ここからは作成したゲーム画面を載せていきたいと思います。また何を実装したかとか何を考えていたかなども書いていこうと思います。
それにしても完成形を見せるのなら動画のほうが伝わりやすいので、noteに動画埋め込めるようになったらいいですね。
スタート画面
スタート画面に操作方法も表示してみました。これは後でキー入力めんどくなりそうだかたらとスペースキーでショット、エンターでゲーム開始と完全に分けました。
もし仮にエンターもショットできるようにするにはゲームのフラグ?スタート画面かプレイ中で分けるんですかね?
それともスタート画面の時のエンター入力はゲーム開始で、ゲーム開始以降はショットみたいな?
……みたいなことを考えていたら考えるのがめんどくさくなったのでゲーム開始はエンター、ショットはスペースキー分けました。
プレイ画面
というわけでプレイ画面。敵としてコウモリとオバケを表示させています。前回はブロックのステータスが全部同じだったので、今回は複数種類のステータスを実装してみたかったので2種類作りました。
コウモリとオバケを選んだ理由としては10月後半でハロウィンが近かったからですね🎃
なおご覧の通り記事投稿時にはすでにハロウィン終わっています👻
ちなみにオバケは↓方向のみの移動で、コウモリは↘︎方向に移動して壁で左右反転します。オバケとコウモリはランダムで定期的に出現し、画面上の敵全てをショットで倒したらゲームクリア、オバケかコウモリのどちらかが画面下に行ったらゲームオーバーです。
緑の四角が弾ですね。これが敵に当たると敵が消えます。黒い長方形が自機です。この二つももっと色を変えたり、特に自機の方はオバケやコウモリと同じく何かしら画像を作ってそれっぽくしたかったのですが自分で作れる簡単なデザインが思いつかず断念しました。
ちなみにコウモリは画像を2種類用意して、交互に表示させることでとんでいるように見えるようにしました。元が小さい画像なので見にくいかもしれませんが載せておきます。
オバケのほうも手とかしっぽ?を変えて動いてる風にしたかったけどこちらも断念しました。オバケの目を><にして被弾したときにその画像を表示させてみたかったんですけど、これもなんかよくわからず断念しました。
まとめ
今回制作したのはだいだいこんな感じです。これになるまで何度もエラーになってくじけそうでしたがなんとかなってよかったです。手こずったのは敵の当たり判定、コウモリの画像を交互に表示させる、敵をランダムに出現させる、敵と弾の配列の管理などだった気がします。
敵の当たり判定に至っては、弾が敵にあたって弾は消滅するのに、敵は消滅しないとかでかなりてこずりました。ほかにもめっちゃ苦戦していたのでその質問がチャットgptに残っているので、苦戦したところとどう改善したかを乗せようと思ったんですが、少し見返しただけでうぇ…となったので止めます。チャットgptに質問してコードが返ってきてもそれ単体では機能しないことも多く、自分どこがどうなっているから機能しないのかを考えるのは大変でした。
なんとなく
コードは機能しているが設定が甘いため想定した実行にならない
実装したいコードに使う変数がおかしい
実装したいコードの定義がおかしい
とかがエラー吐く大きな要因なんですかね。知らんけど。
今回は断念した機能が多いので次つくるとしたらその中の一個でも実装できたらいいなと思います。前ほど時間が取れないため作るガッツがあるかわかりませんが…。
さいごに
次のほうでは備忘録かねてまたjs載せていきます。ほかにもこのゲームのフォルダにhtmlとかcssとかその他色々入ってるんですが、省略してのせます。
コピペだけでは上手くきのうしないので、使う場合はなんかうまいこと変更してください。
ここまで読んでくださりありがとうございました。
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
//キャラクター関連の変数
const charaHeight = 20;
const charaWidth = 40;
//始点
let x =canvas.width/2;
let y =canvas.height - 30;
let charaX =(canvas.width-charaWidth)/2;
const charaSpeed = 7;
let charaColor = "black";
//敵関連の変数
let enemies = []; // 敵を管理する配列
let enemyNumber = 0;
const enemyWidth = 30; // 敵の幅
const enemyHeight = 30; // 敵の高さ
//お化け
let enemy1Image = new Image();
enemy1Image.src = 'img/enemy1.png'; // 画像のパスを指定
// 蝙蝠のアニメーション用の画像を読み込む
let batImages = [new Image(), new Image()];
batImages[0].src = 'img/enemy2.png';
batImages[1].src = 'img/enemy1-2.png';
// 現在の蝙蝠の画像インデックス
let currentBatImageIndex = 0;
// 500msごとに画像を切り替える
setInterval(function() {
currentBatImageIndex = (currentBatImageIndex + 1) % batImages.length;
}, 500);
//弾関連の変数
let shots =[];
let shotColor ="green";
const shotWidth = 10;
const shotHeight = 10;
const shotSpeed = 5;
//フラグに関連する関数
let isGameRunning = false; //ゲーム開始を制御するフラグ
let isGameOver = false; // ゲームオーバー状態を管理するフラグ
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
let rightPressed = false;
let leftPressed = false;
//スタート画面
function drawStartMessage() {
isGameRunning = false;
ctx.clearRect(0, 0, canvas.width, canvas.height); // 画面をクリア
ctx.font = "26px Arial";
ctx.fillStyle = "rgb(112, 162, 172)";
ctx.textAlign = "center";
ctx.fillText("Enterキーでゲーム開始", canvas.width / 2, canvas.height / 2.5);
ctx.fillText("スペースキーでショット",canvas.width/2,canvas.height/2.5+50);
ctx.fillText("← →で左右に移動",canvas.width/2,canvas.height/2.5+100);
}
function handleGameStart() {
if (!isGameRunning) {
isGameRunning = true;
draw();
}
}
document.addEventListener("keydown", function(e) {
if (e.key === "Enter") {
handleGameStart();
}
});
function drawchara(){
ctx.beginPath();
ctx.fillStyle = charaColor;
ctx.fillRect(charaX,y,charaWidth,charaHeight);
ctx.closePath();
}
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
} else if (e.code === "Space") {
shoot();
}}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}
function shoot(){
const shotX = charaX + (charaWidth / 2) - 5; // キャラクターの中央から発射
const shotY = canvas.height - charaHeight - 15; // キャラクターの上から発射
shots.push({ x: shotX, y: shotY }); // 新しい弾を配列に追加
}
function drawShots() {
ctx.fillStyle = shotColor;
for (let i = 0; i < shots.length; i++) {
const shot = shots[i];
ctx.fillRect(shot.x, shot.y, shotWidth, shotHeight); // 弾を描画
shot.y -= shotSpeed; // 弾を上に移動
// 弾が画面外に出た場合、配列から削除
if (shot.y < 0) {
shots.splice(i, 1);
i--; // インデックスを調整
}
}
}
function clearMessage(){
ctx.clearRect(0, 0, canvas.width, canvas.height); // 画面をクリア
ctx.font = "26px Arial";
ctx.fillStyle = "rgb(112, 162, 172)";
ctx.textAlign = "center";
ctx.fillText("CLEAR!!", canvas.width / 2, canvas.height / 2);
}
function checkBulletEnemyCollisions() {
for (let i = shots.length - 1; i >= 0; i--) {
const shot = shots[i];
for (let j = enemies.length - 1; j >= 0; j--) {
const enemy = enemies[j];
if (enemy.status === 1) {
// 衝突判定
if (
shot.x < enemy.x + enemyWidth && // 弾の右側が敵の左側より右にある
shot.x + shotWidth > enemy.x && // 弾の左側が敵の右側より左にある
shot.y < enemy.y + enemyHeight && // 弾の下側が敵の上側より下にある
shot.y + shotHeight > enemy.y // 弾の上側が敵の下側より上にある
)
{
// 衝突が発生した場合
enemy.status = 0; // 敵を消す(ステータスを0にする)
shots.splice(i, 1); // 弾を配列から削除
// ステータスが0の敵を配列から削除
enemies = enemies.filter(enemy => enemy.status === 1);
console.log("敵を倒しました。現在の敵の数: " + enemies.length);
break; // 内部ループを抜ける
} //else {
// 衝突が起きていない場合の確認
// console.log("No collision. Shot:", shot.x, shot.y, "Enemy:", enemy.x, enemy.y);
}
}
}
}
// 敵を追加する関数
function addEnemy(x, y) {
const enemyType = Math.random() < 0.5 ? 'enemy1' : 'enemy2';
let enemyImage;
if (enemyType === 'enemy1') {
enemyImage = enemy1Image; // enemy1の画像
} else {
enemyImage = batImages[0]; // 蝙蝠のアニメーションの最初の画像を使用
}
enemies.push({
x: x,
y: y,
width: enemyWidth, // 幅を追加
height: enemyHeight, // 高さを追加
image: enemyImage,
type: enemyType,
status: 1,
direction: 1 // 初期の方向(1: 右、-1: 左)
});
enemyNumber = enemies.length; // 新しい敵が追加された後、再度長さを更新
isGameRunning = true;
}
// 敵を生成する関数
function spawnEnemy() {
if (isGameOver || !isGameRunning) return;
let enemyX = Math.floor(Math.random() * (canvas.width - enemyWidth));
const enemyType = Math.random() < 0.5 ? 'enemy1' : 'enemy2';
let enemyImage;
if (enemyType === 'enemy1') {
enemyImage = enemy1Image; // enemy1の画像
} else {
enemyImage = batImages; // 蝙蝠のアニメーション画像配列
}
// 敵オブジェクトを配列に追加
enemies.push({
x: enemyX,
y: 0, // 敵は画面の上から出現
width: enemyWidth,
height: enemyHeight,
image: enemyImage,
type: enemyType,
status: 1, // 生存状態
direction: 2 // 初期の方向(1: 右、-1: 左)
});
enemyNumber = enemies.length;
console.log("敵が追加されました: ", enemyNumber);
}
// 敵を描画する関数
function drawEnemies() {
enemies.forEach(enemy => {
if (enemy.status === 1) {
if (enemy.type === 'enemy1') {
ctx.drawImage(enemy.image, enemy.x, enemy.y, enemy.width, enemy.height);
} else if (enemy.type === 'enemy2') {
// 蝙蝠のアニメーション画像を描画
ctx.drawImage(batImages[currentBatImageIndex], enemy.x, enemy.y, enemy.width, enemy.height);
}
}
});
}
function moveEnemies() {
for (let i = 0; i < enemies.length; i++) {
let enemy = enemies[i];
// 敵の色によって動きの条件を変更
if (enemy.type === 'enemy2') { // 蝙蝠の場合
enemy.x += enemy.direction; // 右に移動
enemy.y += 0.5; // 下に移動(斜め移動)
// 壁との衝突判定
if (enemy.x + enemy.width > canvas.width || enemy.x < 0) {
enemy.direction *= -1; // 方向を反転
}
// 蝙蝠の画像を切り替え
enemy.image = batImages[currentBatImageIndex];
} else if (enemy.type === 'enemy1') { // 黒い敵の場合
enemy.y += 1; // 黒い敵は下に移動
}
// 残りの敵の数を自動的に更新
enemyNumber = enemies.length;
console.log("Remaining enemies:", enemyNumber);
}
}
function checkGameOver() {
for (let enemy of enemies) {
if (enemy.y + enemyHeight >= canvas.height) {
alert("Game Over!");
isGameOver = true;
break;
}
}
}
function checkClearCondition() {
// 敵の数が0で、かつゲームが始まっていたらクリアメッセージを表示
if (enemies.length === 0 &&isGameRunning) {
clearMessage();
isGameRunning = false; // クリア後にゲームを終了状態にする
}
}
function resetGame() {
isGameOver = false; // ゲームオーバー状態を初期化
}
function draw() {
if (isGameOver||!isGameRunning) return;
ctx.clearRect(0,0,canvas.width,canvas.height);
if(rightPressed) {
charaX += charaSpeed;
if(charaX + charaWidth > canvas.width) {
charaX = canvas.width - charaWidth;
}
}else if (leftPressed) {
charaX -= charaSpeed;
if(charaX < 0) {
charaX = 0;
}
}
if (enemies.length === 0 && isGameRunning) {
clearMessage(); // クリアメッセージ表示
}
drawchara();
drawShots(); // 弾を描画
drawEnemies(); // 敵を描画
checkBulletEnemyCollisions();
moveEnemies();
checkGameOver();
// 毎フレーム敵の数を確認
checkClearCondition();
requestAnimationFrame(draw); // 次のフレームをリクエスト
}
window.onload = function() {
drawStartMessage(); // クリックを待つメッセージを表示
};
// 例として敵を追加
addEnemy(100, 50);
addEnemy(200, 100);
addEnemy(300, 150);
setInterval(spawnEnemy, 2000);
draw();