0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SQLコーディングゲーム開発記】爆破演出の追加

Posted at

アプリのコード

APP.js
  const handleRun = () => {
    const sqlLower = sql.toLowerCase();
    if (
      sqlLower.includes("select") &&
      sqlLower.includes("age") &&
      sqlLower.includes(">=") &&
      sqlLower.includes("30")
    ) {
      // Step 1: Three.js 爆発演出開始
      setShowExplosionThree(true);

      setTimeout(() => {
        // Step 2: Three.js 終了、Explosion.js 開始
        setShowExplosionThree(false);
        setShowExplosion(true);

        // Step 3: Explosion.js 終了
        setTimeout(() => setShowExplosion(false), 1000);
      }, 1000);
    } else {
      alert("❌ 条件が合っていません");
    }
APP.js
return (
  //実行するボタンでhandleRunを実行。
  <button
     onClick={handleRun}
     className="w-full bg-orange-500 text-white py-2 rounded hover:bg-orange-600 transition"
        >
       ▶️ 実行する
    </button>
);

爆発のコード

Explosion.js
import React, { useEffect, useRef } from 'react';

// Three.jsライブラリ全体を THREE としてインポートします。
import * as THREE from 'three';

const Explosion = () => {

 //DOMへの参照を取得するための ref。ここにThree.jsのcanvas要素をマウントします。
  const mountRef = useRef(null);

  useEffect(() => {
   
    // ここでローカル変数にコピー
    const mount = mountRef.current; 
    //Three.jsで描画される「シーン」を作成します。
    const scene = new THREE.Scene();

    // カメラの設定
    //視野角75度、画面サイズに合わせたアスペクト比、0.1〜1000の視点距離範囲のカメラを作成。
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );

    //カメラをZ軸方向に15離して、オブジェクト全体が見えるようにします。
    camera.position.z = 15;

    // レンダラー
    // WebGLレンダラーを生成。アンチエイリアスで滑らかに、透過は無効。
    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
    //レンダラーのサイズを画面全体にします。
    renderer.setSize(window.innerWidth, window.innerHeight);
    //背景色を黒に設定(不透明)。
    renderer.setClearColor(0x000000, 1);
    //canvas(DOM)を実際のHTML要素に追加して表示。
    mount.appendChild(renderer.domElement);

    // 粒子群を作成
   // パーティクル(爆発の粒子)200個を格納する配列と、下向きの重力ベクトルを定義。
    const particles = [];
    const particleCount = 200;
    const gravity = new THREE.Vector3(0, -0.03, 0); // 少し強めの重力

    //200個のパーティクルを1つずつ作成。
    for (let i = 0; i < particleCount; i++) {
    // 	小さな球体(爆発の粒)の形状を定義。
      const geometry = new THREE.SphereGeometry(0.12, 8, 8);
      //明るめのオレンジ〜黄色の色相をもつ素材(透明度も設定)を作成。
      const material = new THREE.MeshBasicMaterial({
        color: new THREE.Color(`hsl(${30 + Math.random() * 30}, 100%, 50%)`),
        transparent: true,
        opacity: 1.0,
      });
     //形状と素材から1つの粒子メッシュを生成。
      const particle = new THREE.Mesh(geometry, material);
      //初期位置を画面中央に。
      particle.position.set(0, 0, 0);

      // ランダムな球面方向に速度を与える
    //球面座標系で、ランダム方向に速度ベクトルを与えるための角度と速度を生成。
      const theta = Math.random() * 2 * Math.PI;
      const phi = Math.acos(2 * Math.random() - 1);
      const speed = 4 + Math.random() * 6;

	//速度ベクトルを3次元で作成し、速度倍率をかけて実際の移動量を決定。
      const velocity = new THREE.Vector3(
        Math.sin(phi) * Math.cos(theta),
        Math.sin(phi) * Math.sin(theta),
        Math.cos(phi)
      ).multiplyScalar(speed);

    //粒子のデータに速度と「生存時間」(アニメーション経過時間)を追加。
      particle.userData.velocity = velocity;
      particle.userData.aliveTime = 0; // 生存時間カウント
      
    //シーンに追加し、管理配列にも保存。
      scene.add(particle);
      particles.push(particle);
    }

    // 爆発のフラッシュ光
    //フラッシュ(爆発時の光)用の半透明の黄色っぽいマテリアル。
    const flashMaterial = new THREE.MeshBasicMaterial({
      color: 0xffffcc,
      transparent: true,
      opacity: 1.0,
      side: THREE.DoubleSide,
    });
    //中央に発光球体を置き、シーンに追加。
    const flash = new THREE.Mesh(new THREE.SphereGeometry(1.5, 32, 32), flashMaterial);
    scene.add(flash);

    // アニメーション開始時刻
    let startTime = null;
    const maxDuration = 3500; // 3.5秒でフェードアウト終了

    // アニメーション関数と、その呼び出しIDを定義。
    let frameId;
    const animate = (time) => {
    
    //	初回のみ開始時間を保存し、現在までの経過時間を計算。
      if (!startTime) startTime = time;
      const elapsed = time - startTime;

      particles.forEach((p) => {
        // 重力加算
        p.userData.velocity.add(gravity);

        // 位置更新
        p.position.add(p.userData.velocity);

        // 生存時間アップデート
        p.userData.aliveTime += 16;

        // 徐々に透明にし縮小
        p.material.opacity = THREE.MathUtils.clamp(1 - p.userData.aliveTime / maxDuration, 0, 1);
        const scaleVal = THREE.MathUtils.lerp(1, 0, p.userData.aliveTime / maxDuration);
        p.scale.setScalar(scaleVal);
      });

      // フラッシュも徐々に大きく、透明に
      flash.scale.multiplyScalar(1.1);
      flash.material.opacity = THREE.MathUtils.clamp(1 - elapsed / maxDuration, 0, 1);

    //毎フレームの描画処理。
      renderer.render(scene, camera);

      //アニメーションが時間内であれば、次のフレームを予約。
      if (elapsed < maxDuration) {
        frameId = requestAnimationFrame(animate);
      }
    };
    //最初のフレームを呼び出してアニメーションを開始。
    frameId = requestAnimationFrame(animate);
    //クリーンアップ関数。Reactのアンマウント時に呼ばれる。
    return () => {
    //アニメーション停止、canvas削除、メモリ解放。
      cancelAnimationFrame(frameId);
      if (mount && renderer.domElement) {
        mount.removeChild(renderer.domElement);
      }
      renderer.dispose();
    };
    //useEffectがマウント時に一度だけ実行されることを意味します。
  }, []);

    //Three.jsのcanvasを挿入するための空のdiv。全画面に固定表示され、クリックなどのイベントをブロックしません。
  return (
    <div
      ref={mountRef}
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100vw',
        height: '100vh',
        pointerEvents: 'none',
        zIndex: 9999,
      }}
    />
  );
};

export default Explosion;

爆発コードの不明点まとめ

  • Three.jsライブラリ
    Webブラウザ上で3Dグラフィックスを表示・操作するためのJavaScriptライブラリです。
  • canvas要素
    HTML5で導入された描画専用のタグです。これを使うことで、JavaScriptを使って図形・画像・アニメーション・3D表現などを直接描画できる領域をウェブページ上に作れます。
  • WebGLレンダラー
    WebGLの複雑なAPIを隠し、直感的な記述で3Dを実現する
  • アンチエイリアス(WebGLレンダラー)
    3Dオブジェクトのエッジ(境界線)をなめらかに描画する処理のことです。
  • canvas(DOM)
    HTML5で導入された描画専用のタグです。これを使うことで、JavaScriptを使って図形・画像・アニメーション・3D表現などを直接描画できる領域をウェブページ上に作れます。
  • Math.sin
    三角関数のサイン (sin) 計算するための JavaScript 組み込み関数。
    xラジアンのsin値(y座標)
  • Math.cos
    三角関数のコサイン (cos) 計算するための JavaScript 組み込み関数。
    xラジアンのcos値(x座標)

爆弾のコード

ExplosionThree.js
import React, { useEffect, useRef } from "react";
import * as THREE from "three";

const ExplosionThree = () => {
  const mountRef = useRef(null);

  useEffect(() => {
    const mount = mountRef.current;

    // シーンセットアップ
    const scene = new THREE.Scene();

    // カメラセットアップ
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    camera.position.z = 5;

    // レンダラーセットアップ
    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    
    //rendererのcanvas要素を div に追加(DOMに表示)。
    mount.appendChild(renderer.domElement);

    // 🧨 爆弾本体(黒い球体)
    // 半径0.5の球体を作成。頂点数が多めで滑らか。
    const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
    //真っ黒なマテリアル。透明度を操作可能にしています。
    const sphereMaterial = new THREE.MeshBasicMaterial({
      color: 0x111111,
      transparent: true,
      opacity: 1,
    });
    //ジオメトリとマテリアルを結合して「爆弾メッシュ」を作成。
    const bomb = new THREE.Mesh(sphereGeometry, sphereMaterial);
    scene.add(bomb);

    // 🧵 導火線(シリンダー)
    const fuseGeometry = new THREE.CylinderGeometry(0.05, 0.05, 0.3, 8);
    const fuseMaterial = new THREE.MeshBasicMaterial({ color: 0xffa500 }); // オレンジ
    const fuse = new THREE.Mesh(fuseGeometry, fuseMaterial);
    fuse.position.y = 0.6; // 球の上に乗せる
    bomb.add(fuse); // 導火線を爆弾に「子オブジェクト」として追加(爆弾と一緒に動くように)。

    // 🔥 火花(オプション: 点滅)
    const sparkGeometry = new THREE.SphereGeometry(0.05, 8, 8);
    //黄色のマテリアル。
    const sparkMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
    //火花のメッシュを作成。
    const spark = new THREE.Mesh(sparkGeometry, sparkMaterial);
    spark.position.y = 0.2;
    fuse.add(spark);

    // ウィンドウのリサイズ時に、カメラとレンダラーを再調整。
    const handleResize = () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    };
    //イベントリスナーとして登録。
    window.addEventListener("resize", handleResize);
    //requestAnimationFrame のID保存用。
    let reqId;

    // アニメーション関数
      //毎フレーム描画するループ処理(再帰的に自分を呼ぶ)。
    const animate = () => {
      reqId = requestAnimationFrame(animate);

      // スケールアップ
      bomb.scale.x += 0.01;
      bomb.scale.y += 0.01;
      bomb.scale.z += 0.01;

      // フェードアウト
      if (bomb.material.opacity > 0) {
        bomb.material.opacity -= 0.01;
        if (bomb.material.opacity < 0) bomb.material.opacity = 0;
      }

      // 点滅(火花用)
      //火花が点滅するように、ランダムに表示/非表示を切り替える。
      spark.visible = Math.random() > 0.5;

      renderer.render(scene, camera);
    };

    animate();

    // クリーンアップ
    return () => {
    //アニメーションループ停止。
      cancelAnimationFrame(reqId);
      //リサイズイベント解除。
      window.removeEventListener("resize", handleResize);
      if (mount && renderer.domElement) {
        mount.removeChild(renderer.domElement);
      }
      //レンダラーを破棄してメモリ解放。
      renderer.dispose();
    };
  }, []);
	//refを関連付けた透明なdiv。ここにThree.jsのcanvasが描画される。
	//画面全体に固定表示(fixed)、クリックなどを受け付けない(pointerEvents: none)。
  return (
    <div
      ref={mountRef}
      style={{
        position: "fixed",
        top: 0,
        left: 0,
        width: "100vw",
        height: "100vh",
        pointerEvents: "none",
        zIndex: 9999,
      }}
    />
  );
};

export default ExplosionThree;

UI

画面収録 2025-06-22 9.10.09.gif

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?