Newer
Older
PixelPaintWar / apps / client / src / scenes / GameScene.tsx
@rinto hasegawa rinto hasegawa on 21 Feb 7 KB [refctor] コメントの整理
import { useEffect, useRef } from "react";
import { Application, Container } from "pixi.js";

// ネットワーク・入力関連
import { socketClient} from "../network/SocketClient";
import { VirtualJoystick, MAX_DIST } from "../input/VirtualJoystick";
import { GAME_CONFIG } from "@repo/shared/src/config/gameConfig";
import { type PlayerData } from "@repo/shared/src/types/player";

// ゲーム描画オブジェクト
import { GameMap } from "../entities/GameMap";
import { Player } from "../entities/Player";

interface GameSceneProps {
  myId: string | null;
}

/**
 * メインゲーム画面コンポーネント
 * PixiJS 初期化・エンティティ管理・ソケット同期処理
 */
export function GameScene({ myId }: GameSceneProps) {
  const pixiContainerRef = useRef<HTMLDivElement>(null);
  // ジョイスティック入力値
  const joystickInputRef = useRef({ x: 0, y: 0 });
  // プレイヤースプライト参照テーブル
  const playersRef = useRef<Record<string, Player>>({});
  // 位置送信間隔制御用タイムスタンプ
  const lastPositionSentTimeRef = useRef<number>(0);
  // 他プレイヤー補間用目標座標テーブル
  const targetPositionsRef = useRef<Record<string, { x: number, y: number }>>({});
  // 前フレーム移動状態
  const wasMovingRef = useRef<boolean>(false);

  useEffect(() => {
    if (!pixiContainerRef.current) return;

    // 非同期初期化中アンマウント判定フラグ
    let isCancelled = false;
    // Pixi 初期化完了フラグ
    let isInitialized = false;
    const app = new Application();
    // カメラ追従向けワールド親コンテナ
    const worldContainer = new Container();

    const initPixi = async () => {
      // 背景マップ最背面配置
      const gameMap = new GameMap();
      worldContainer.addChild(gameMap);

      // ソケットイベント購読登録

      // 参加済みプレイヤー初期スプライト生成
      socketClient.onCurrentPlayers((serverPlayers: PlayerData[] | Record<string, PlayerData>) => {
        console.log("🔥 プレイヤー一覧を受信:", serverPlayers);
        const playersArray = (Array.isArray(serverPlayers) ? serverPlayers : Object.values(serverPlayers)) as PlayerData[];
        playersArray.forEach((p) => {
          const isMe = p.id === myId;
          const playerSprite = new Player(GAME_CONFIG.TEAM_COLORS[p.teamId], isMe);
          playerSprite.position.set(p.x, p.y);
          worldContainer.addChild(playerSprite);
          playersRef.current[p.id] = playerSprite;
          targetPositionsRef.current[p.id] = { x: p.x, y: p.y };
        });
      });

      // 新規参加プレイヤー追加処理
      socketClient.onNewPlayer((p: PlayerData) => {
        console.log("🔥 新規プレイヤー参加:", p);
        const playerSprite = new Player(GAME_CONFIG.TEAM_COLORS[p.teamId], false);
        playerSprite.position.set(p.x, p.y);
        worldContainer.addChild(playerSprite);
        playersRef.current[p.id] = playerSprite;
        targetPositionsRef.current[p.id] = { x: p.x, y: p.y };
      });

      // 他プレイヤー目標座標更新
      socketClient.onUpdatePlayer((data: Partial<PlayerData> & { id: string }) => {
        if (data.id === myId) return;
        
        const targetPos = targetPositionsRef.current[data.id];
        if (targetPos) {
          if (data.x !== undefined) targetPos.x = data.x;
          if (data.y !== undefined) targetPos.y = data.y;
        }
      });

      // 退出プレイヤー参照削除とオブジェクト破棄
      socketClient.onRemovePlayer((id: string) => {
        const target = playersRef.current[id];
        if (target) {
          worldContainer.removeChild(target);
          target.destroy();
          delete playersRef.current[id];
          delete targetPositionsRef.current[id];
        }
      });

      // PixiJS 本体初期化
      await app.init({ resizeTo: window, backgroundColor: 0x111111, antialias: true });
      isInitialized = true;
      
      // 初期化待機中アンマウント時の即時破棄分岐
      if (isCancelled) {
        app.destroy(true, { children: true });
        return;
      }
      
      // ルートステージへのワールド追加
      pixiContainerRef.current?.appendChild(app.canvas);
      app.stage.addChild(worldContainer);

      // 画面準備完了通知送信
      socketClient.readyForGame();

      // メインゲームループ登録
      app.ticker.add((ticker) => {
        if (!myId) return;
        const me = playersRef.current[myId];
        if (!me) return; 

        // 自プレイヤー移動処理
        const { x: dx, y: dy } = joystickInputRef.current;
        const isMoving = dx !== 0 || dy !== 0;
        
        if (isMoving) {
          me.move(dx / MAX_DIST, dy / MAX_DIST, ticker.deltaTime);
          
          // 通信負荷抑制向け間引き送信
          const now = performance.now();
          if (now - lastPositionSentTimeRef.current >= GAME_CONFIG.PLAYER_POSITION_UPDATE_MS) {
            socketClient.sendMove(me.x, me.y);
            lastPositionSentTimeRef.current = now;
          }
        } else if (wasMovingRef.current) {
          // 停止瞬間の確定座標単発送信
          socketClient.sendMove(me.x, me.y);
        }

        // 次フレーム比較用移動状態保存
        wasMovingRef.current = isMoving;

        // 他プレイヤー座標の線形補間と閾値吸着
        Object.entries(playersRef.current).forEach(([id, player]) => {
          if (id === myId) return;

          const targetPos = targetPositionsRef.current[id];
          if (targetPos) {
            const diffX = targetPos.x - player.x;
            const diffY = targetPos.y - player.y;

            // X軸方向補間と吸着
            if (Math.abs(diffX) < GAME_CONFIG.PLAYER_LERP_SNAP_THRESHOLD) {
              player.x = targetPos.x;
            } else {
              player.x += diffX * GAME_CONFIG.PLAYER_LERP_SMOOTHNESS * ticker.deltaTime;
            }

            // Y軸方向補間と吸着
            if (Math.abs(diffY) < GAME_CONFIG.PLAYER_LERP_SNAP_THRESHOLD) {
              player.y = targetPos.y;
            } else {
              player.y += diffY * GAME_CONFIG.PLAYER_LERP_SMOOTHNESS * ticker.deltaTime;
            }
          }
        });

        // 自プレイヤー中心表示向けワールド逆方向オフセット
        worldContainer.position.set(
          -(me.x - app.screen.width / 2),
          -(me.y - app.screen.height / 2)
        );
      });
    };

    initPixi();

    // コンポーネント破棄時クリーンアップ
    return () => {
      isCancelled = true;

      if (isInitialized) {
        app.destroy(true, { children: true });
      }

      playersRef.current = {};
      
      // メモリリーク防止向けイベント購読解除
      socketClient.socket.off("current_players");
      socketClient.socket.off("new_player");
      socketClient.socket.off("update_player");
      socketClient.socket.off("remove_player");
    };
  }, [myId]);

  return (
    <div style={{ width: "100vw", height: "100vh", overflow: "hidden", position: "relative", backgroundColor: "#000" }}>
      {/* PixiJS Canvas 配置領域 */}
      <div ref={pixiContainerRef} style={{ position: "absolute", top: 0, left: 0, zIndex: 1 }} />
      
      {/* 入力UI重畳用前面レイヤー */}
      <div style={{ position: "absolute", zIndex: 2, width: "100%", height: "100%" }}>
        <VirtualJoystick onMove={(x, y) => { joystickInputRef.current = { x, y }; }} />
      </div>
    </div>
  );
}