このサイトはアフィリエイトリンクを含んでいます

【HTML5ゲーム開発】ここで一服。PCブラウザ画面と端末によって違うスマホ画面に画面サイズを最適化。その方法と解説! Ver1.7

ゲーム画面の最適化 作ってみた!
あんちゃん
あんちゃん

PCブラウザ版での操作方法はこれさ
zキーが弾丸発射!

左右矢印キーで機体を左右に動かせるよ!

スタートするときはマウスで「スタート」をクリックしてね!

こんにちは!ブログ「アンブタニウムシード」のあんちゃんです!今回は、HTML5で作ったシューティングゲームのバージョン1.7.37について、みなさんにわかりやすく解説します。このゲームはPCブラウザとスマホでの見え方や操作感を最適化するために、37回のマイナーアップデートを繰り返して完成しました。どちらのデバイスでも楽しくプレイできるように工夫しているので、ぜひ見てくださいね!

ゲームの基本構造とスタイル設定

ゲームのHTMLファイルは、<!DOCTYPE html>から始まる標準的なHTML5文書です。<head>タグの中には、文字コードを指定する<meta charset="UTF-8">や、ページのタイトルを指定する<title>タグがあります。このタイトルは、ブラウザのタブに表示されるものです。ページ全体のスタイルは、<style>タグ内にCSSで記述されています。

スタイル設定のポイント

  • 背景画像と全体のレイアウト: ゲームの背景には、宇宙のイメージが使用されており、CSSのbackground-imageプロパティで指定されています。この背景画像はページ全体に広がり、中央に配置されるように設定されています。background-sizecoverに設定されているので、画像が切れずに画面全体にフィットします。
  • ゲームコンテナとキャンバス: ゲームが描画される部分は<canvas>要素で、これは#gameCanvasとして定義されています。このキャンバスは<div>要素である#gameContainer内に配置され、ここでのゲームの描画やプレイが行われます。#gameContainerはページ全体のレイアウトを制御し、中央に表示されるように設定されています。

ゲーム要素のレイアウト

  • スコア表示とスタートボタン: #scoreContainerはスコア表示とスタートボタンを含むエリアです。画面の上部に固定されており、スコアはリアルタイムで更新されます。背景色には半透明の白を使用しており、スコアが見やすくなっています。
  • 仮想コントロールボタン: #controlsには、スマホ版での操作に使用する仮想コントロールボタンが含まれています。左移動、右移動、射撃のボタンがあり、スマホでも快適にプレイできるようになっています。PCブラウザ版ではこのボタンは非表示にされており、キーボードでの操作がメインとなります。

ゲームの描画と操作

  • 背景の星の描画: drawStarsという関数が背景に星をランダムに描画します。星の位置や大きさはランダムで決まり、これがゲームの宇宙空間の雰囲気を演出しています。
  • 自機と敵機、弾丸の管理: 自機(プレイヤーキャラクター)は、drawShip関数で描画され、ユーザーの操作に応じて左右に移動します。敵機や弾丸もそれぞれ専用の関数で描画され、衝突判定や動きがリアルタイムで処理されます。敵機は上から降りてきて、弾丸を発射してくるので、それを避けつつ撃ち落とすのがプレイヤーの目標です。
  • 爆発エフェクト: 敵機を撃ち落としたときや、自機がダメージを受けたときには爆発エフェクトが発生します。これは、複数の画像をフレームごとに切り替えることでアニメーション効果を出しています。

ゲームロジックとインタラクション

  • イベントリスナー: キーボードやタッチ操作を監視するためのイベントリスナーが設定されています。これにより、ユーザーの操作に応じてゲームの状態が更新されます。例えば、keydownイベントで自機の移動が制御され、keypressイベントで弾丸を発射することができます。
  • レスポンシブデザイン: メディアクエリを使用して、PCブラウザ版とスマホ版で異なるスタイルが適用されるようにしています。これにより、画面サイズやデバイスの種類に応じて最適な表示が行われます。特に、自機の位置やボタンの表示はデバイスごとに調整されています。
  • スコアとゲームオーバーの表示: ゲーム中のスコアは#scoreDisplayに表示され、ゲームオーバーになると#finalScoreが表示されます。ゲームオーバー画面では最終スコアが表示され、再度スタートボタンを押すことでゲームがリスタートします。

このゲームは、PCブラウザとスマホの両方で快適にプレイできるように、細部までこだわって作られています。これで、どこでも楽しく遊べますね!今後もさらに楽しいゲームを作っていきますので、お楽しみに!

スマホ画面に思うこと
  • 端末によって画面サイズが違う
  • タッチ操作の為、専用ボタンが必須
PCブラウザ画面に思うこと
  • 人によってウィンドウサイズが違う
  • キーボードで操作できる
あんちゃん
あんちゃん

はぁはぁ、、つ、ついにできた。。

スマホ版とPCブラウザ版でどちらでもレイアウトが崩れないようにできましたよ。

参考にしてみてね!

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Shooting Game Ver1.7.36</title>
  <style>
    body, html {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
      display: flex;
      flex-direction: column;
      background-image: url('https://tokodomo.xyz/wp-content/uploads/2024/07/shooting_ver1.7_background.webp');
      background-size: cover;
      background-position: center;
    }
    #gameContainer {
      position: relative;
      width: 100%;
      flex-grow: 1;
      display: flex;
      justify-content: center;
      align-items: center;
      background-color: rgba(0, 0, 0, 0.7);
      z-index: 5;
    }
    #gameCanvas {
      width: 100%;
      height: 100%;
      display: block;
      object-fit: contain;
    }
    #scoreContainer {
      font-size: 20px;
      background-color: rgba(255, 255, 255, 0.7);
      padding: 10px;
      position: fixed;
      top: 0;
      width: 100%;
      display: flex;
      justify-content: space-between;
      box-sizing: border-box;
      z-index: 10;
    }
    #startButton {
      flex: none;
    }
    #finalScore {
      font-size: 30px;
      color: red;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: rgba(255, 255, 255, 0.7);
      padding: 10px;
      display: none;
      width: 100%;
      text-align: center;
    }
    #controls {
      position: fixed;
      bottom: 0;
      width: 100%;
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      align-items: center;
      padding: 10px 20px;
      box-sizing: border-box;
      z-index: 10;
      background-color: rgba(0, 0, 0, 0.5);
    }
    .control-button {
      width: 60px;
      height: 60px;
      background-color: rgba(255, 255, 255, 0.7);
      border: none;
      border-radius: 50%;
      font-size: 20px;
      font-weight: bold;
      color: black;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    #moveControl {
      width: 140px;
      display: flex;
      justify-content: space-between;
    }
    .connected-button {
      border-radius: 0;
      flex-grow: 1;
      margin: 0;
    }
    .connected-button:first-child {
      border-top-left-radius: 50%;
      border-bottom-left-radius: 50%;
    }
    .connected-button:last-child {
      border-top-right-radius: 50%;
      border-bottom-right-radius: 50%;
    }
    @media (min-width: 601px) {
      #controls {
        display: none;
      }
    }
  </style>
</head>
<body>
  <div id="scoreContainer">
    <div id="scoreDisplay">スコア: 0</div>
    <button id="startButton" onclick="startGame()">START</button>
  </div>
  <div id="gameContainer">
    <canvas id="gameCanvas"></canvas>
    <div id="finalScore">GAME OVER<br>SCORE: 0</div>
  </div>
  <div id="controls">
    <button class="control-button" id="fireButton">&#x1F52B;</button>
    <div id="moveControl">
      <button class="control-button connected-button" id="leftButton">&larr;</button>
      <button class="control-button connected-button" id="rightButton">&rarr;</button>
    </div>
  </div>
  <script>
    // ゲーム設定
    const canvas = document.getElementById("gameCanvas");
    const ctx = canvas.getContext("2d");

    function resizeCanvas() {
      const container = document.getElementById('gameContainer');
      const controlsHeight = window.matchMedia("(max-width: 600px)").matches ? document.getElementById('controls').offsetHeight : 0;
      const width = container.clientWidth;
      const height = container.clientHeight - controlsHeight;
      canvas.height = height;
      canvas.width = width;

      // 自機の位置を設定
      const bottomOffset = 20; // 底からのオフセット(20px)
      if (window.matchMedia("(max-width: 600px)").matches) {
        shipY = canvas.height - shipHeight - bottomOffset - 20; // 追加の20pxオフセット
      } else {
        const canvasRect = canvas.getBoundingClientRect();
        shipY = window.innerHeight - canvasRect.top - shipHeight - bottomOffset - 20; // PC版: 底からさらに20px上げる
      }
      shipX = canvas.width / 2 - shipWidth / 2;
      ship = { x: shipX, y: shipY, width: shipWidth, height: shipHeight, dx: 0 };
    }

    window.addEventListener('resize', resizeCanvas);
    window.addEventListener('load', resizeCanvas);

    // アニメーションフレームID
    let animationFrameId;

    // 背景の星を描画する関数
    function drawStars() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      const numStars = 100;
      for (let i = 0; i < numStars; i++) {
        const x = Math.random() * canvas.width;
        const y = Math.random() * canvas.height;
        const radius = Math.random() * 1.5;
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2);
        ctx.fillStyle = "white";
        ctx.fill();
      }
    }

    // 自機画像の読み込み
    const shipImage = new Image();
    shipImage.src = 'https://tokodomo.xyz/wp-content/uploads/2024/07/jiki.png';

    const shipWidth = 50;
    const shipHeight = 50;
    let shipX = canvas.width / 2 - shipWidth / 2;
    let shipY = canvas.height - shipHeight - 20;

    let ship = { x: shipX, y: shipY, width: shipWidth, height: shipHeight, dx: 0 };
    let bullets = [];
    let enemyBullets = [];
    let enemies = [];
    let explosions = [];
    let score = 0;
    let gameOver = false;
    let gameOverTimeout;

    // 敵機画像の読み込み
    const enemyImage = new Image();
    enemyImage.src = 'https://tokodomo.xyz/wp-content/uploads/2024/07/teki.png';

    // 爆発エフェクト画像の読み込み
    const explosionImages = [
      new Image(),
      new Image()
    ];
    explosionImages[0].src = 'https://tokodomo.xyz/wp-content/uploads/2024/07/shooting_ver1.4_bom1.png';
    explosionImages[1].src = 'https://tokodomo.xyz/wp-content/uploads/2024/07/shooting_ver1.4_bom2.png';

    // 爆発エフェクトの設定
    const explosionFrameWidth = 64;
    const explosionFrameHeight = 64;
    const explosionFrameCount = 16;

    const keys = {};

    document.addEventListener("keydown", keyDownHandler);
    document.addEventListener("keyup", keyUpHandler);
    document.addEventListener("keypress", keyPressHandler);

    function keyDownHandler(e) {
      keys[e.key] = true;

      if (keys["ArrowRight"] && !keys["ArrowLeft"]) {
        ship.dx = 5;
      } else if (keys["ArrowLeft"] && !keys["ArrowRight"]) {
        ship.dx = -5;
      } else if (keys["ArrowRight"] && keys["ArrowLeft"]) {
        ship.dx = 0;
      }
    }

    function keyUpHandler(e) {
      keys[e.key] = false;

      if (keys["ArrowRight"] && !keys["ArrowLeft"]) {
        ship.dx = 5;
      } else if (keys["ArrowLeft"] && !keys["ArrowRight"]) {
        ship.dx = -5;
      } else {
        ship.dx = 0;
      }
    }

    function keyPressHandler(e) {
      if (e.key === "z" || e.key === "Z") {
        bullets.push({ x: ship.x + ship.width / 2 - 2.5, y: ship.y, width: 5, height: 10, dy: -5 });
      }
    }

    function fireBullet(e) {
      e.preventDefault();
      bullets.push({ x: ship.x + ship.width / 2 - 2.5, y: ship.y, width: 5, height: 10, dy: -5 });
    }

    if (window.matchMedia("(max-width: 600px)").matches) {
      document.getElementById("fireButton").addEventListener("touchstart", fireBullet);

      const moveControl = document.getElementById("moveControl");
      let initialTouchX = null;
      const touches = {};

      moveControl.addEventListener("touchstart", (e) => {
        for (const touch of e.touches) {
          touches[touch.identifier] = touch.clientX;
        }
      });

      moveControl.addEventListener("touchmove", (e) => {
        for (const touch of e.touches) {
          const previousX = touches[touch.identifier];
          if (previousX !== undefined) {
            const deltaX = touch.clientX - previousX;
            ship.x += deltaX * 0.9;
            if (ship.x < 0) ship.x = 0;
            if (ship.x + ship.width > canvas.width) ship.x = canvas.width - ship.width;
            touches[touch.identifier] = touch.clientX;
          }
        }
        e.preventDefault();
      });

      moveControl.addEventListener("touchend", (e) => {
        for (const touch of e.changedTouches) {
          delete touches[touch.identifier];
        }
      });

      window.addEventListener("touchstart", function(e) {
        if (e.touches.length > 1) {
          e.preventDefault();
        }
      }, { passive: false });

      window.addEventListener("gesturestart", function(e) {
        e.preventDefault();
      });
      window.addEventListener("gesturechange", function(e) {
        e.preventDefault();
      });
      window.addEventListener("gestureend", function(e) {
        e.preventDefault();
      });
    }

    function drawShip() {
      ctx.drawImage(shipImage, ship.x, ship.y, ship.width, ship.height);
    }

    function drawBullets() {
      ctx.fillStyle = "#FF0000";
      bullets.forEach((bullet, index) => {
        ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
        bullet.y += bullet.dy;
        if (bullet.y < 0) {
          bullets.splice(index, 1);
        }
      });
    }

    function drawEnemyBullets() {
      ctx.fillStyle = "#FFFF00";
      enemyBullets.forEach((bullet, index) => {
        ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
        bullet.y += bullet.dy;
        if (bullet.y > canvas.height) {
          enemyBullets.splice(index, 1);
        }
      });
    }

    function drawEnemies() {
      enemies.forEach((enemy, index) => {
        if (enemy.alpha < 1) {
          enemy.alpha += 0.02;
        }
        ctx.globalAlpha = enemy.alpha;
        ctx.drawImage(enemyImage, enemy.x, enemy.y, enemy.width, enemy.height);
        ctx.globalAlpha = 1.0;
        enemy.y += enemy.dy;
        if (enemy.y > canvas.height + 30) {  // ボタン帯エリアを通過するまで表示
          enemies.splice(index, 1);
        }
        if (Math.random() < 0.01) {
          enemyBullets.push({ x: enemy.x + enemy.width / 2 - 2.5, y: enemy.y + enemy.height, width: 5, height: 10, dy: 5 });
        }
      });
    }

    function createEnemies() {
      if (Math.random() < 0.02) {
        let enemyX = Math.random() * (canvas.width - 30);
        enemies.push({ x: enemyX, y: -100, width: 30, height: 30, dy: 2, alpha: 0 }); // 画面上部外から出現
      }
    }

    function drawExplosions() {
      explosions.forEach((explosion, index) => {
        const frame = Math.floor(explosion.frame / 4);
        if (frame >= explosionFrameCount) {
          explosions.splice(index, 1);
          return;
        }

        const image = explosionImages[Math.floor(Math.random() * explosionImages.length)];

        const explosionX = explosion.x - (explosion.width / 2);
        const explosionY = explosion.y - (explosion.height / 2);

        ctx.drawImage(
          image,
          0, 0, image.width, image.height,
          explosionX, explosionY, explosion.width, explosion.height
        );
        explosion.frame++;
        explosion.y += 1;
      });
    }

    function detectCollisions() {
      bullets.forEach((bullet, bulletIndex) => {
        enemies.forEach((enemy, enemyIndex) => {
          if (
            bullet.x < enemy.x + enemy.width &&
            bullet.x + bullet.width > enemy.x &&
            bullet.y < enemy.y + enemy.height &&
            bullet.y + bullet.height > enemy.y
          ) {
            bullets.splice(bulletIndex, 1);
            enemies.splice(enemyIndex, 1);
            score += 10;
            updateScore();
            const useFirstImage = Math.random() < 0.5;
            explosions.push({
              x: enemy.x + enemy.width / 2,
              y: enemy.y + enemy.height / 2,
              width: enemy.width,
              height: enemy.height,
              frame: 0,
              useFirstImage
            });
          }
        });
      });

      enemies.forEach((enemy, enemyIndex) => {
        if (
          ship.x < enemy.x + enemy.width &&
          ship.x + ship.width > enemy.x &&
          ship.y < enemy.y + enemy.height &&
          ship.y + ship.height > enemy.y
        ) {
          gameOver = true;
          const useFirstImage = Math.random() < 0.5;
          explosions.push({
            x: ship.x + ship.width / 2,
            y: ship.y + ship.height / 2,
            width: ship.width,
            height: ship.height,
            frame: 0,
            useFirstImage
          });
          gameOverTimeout = setTimeout(displayFinalScore, 500);
        }
      });

      enemyBullets.forEach((bullet, bulletIndex) => {
        if (
          bullet.x < ship.x + ship.width &&
          bullet.x + bullet.width > ship.x &&
          bullet.y < ship.y + ship.height &&
          bullet.y + bullet.height > ship.y
        ) {
          gameOver = true;
          const useFirstImage = Math.random() < 0.5;
          explosions.push({
            x: ship.x + ship.width / 2,
            y: ship.y + ship.height / 2,
            width: ship.width,
            height: ship.height,
            frame: 0,
            useFirstImage
          });
          gameOverTimeout = setTimeout(displayFinalScore, 500);
        }
      });
    }

    function updateScore() {
      document.getElementById("scoreDisplay").innerText = `スコア: ${score}`;
    }

    function displayFinalScore() {
      const finalScoreElement = document.getElementById("finalScore");
      finalScoreElement.innerText = `GAME OVER\nSCORE: ${score}`;
      finalScoreElement.style.display = "block";
    }

    function update() {
      if (gameOver && explosions.length === 0) {
        cancelAnimationFrame(animationFrameId);
        return;
      }
      drawStars();
      if (!gameOver) {
        ship.x += ship.dx;
        if (ship.x < 0) ship.x = 0;
        if (ship.x + ship.width > canvas.width) ship.x = canvas.width - ship.width;

        drawShip();
      }

      drawBullets();
      drawEnemyBullets();
      drawEnemies();
      drawExplosions();
      createEnemies();
      detectCollisions();

      animationFrameId = requestAnimationFrame(update);
    }

    function startGame() {
      gameOver = false;
      score = 0;
      ship = { x: shipX, y: shipY, width: shipWidth, height: shipHeight, dx: 0 };
      bullets = [];
      enemyBullets = [];
      enemies = [];
      explosions = [];
      document.getElementById("finalScore").style.display = "none";
      clearTimeout(gameOverTimeout);
      cancelAnimationFrame(animationFrameId);
      update();
    }

    document.addEventListener("DOMContentLoaded", (event) => {
      drawStars();
      resizeCanvas();
    });
  </script>
</body>
</html>

コメント

タイトルとURLをコピーしました