Merge version_3 into main #4
292
src/app/page.tsx
292
src/app/page.tsx
@@ -17,6 +17,192 @@ import {
|
||||
Trophy,
|
||||
Gamepad2,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
type GameState = "idle" | "playing" | "paused" | "gameover";
|
||||
|
||||
interface GameInstance {
|
||||
id: string;
|
||||
state: GameState;
|
||||
score: number;
|
||||
level: number;
|
||||
}
|
||||
|
||||
const GameContainer = ({ gameId }: { gameId: string }) => {
|
||||
const [gameState, setGameState] = useState<GameState>("idle");
|
||||
const [score, setScore] = useState(0);
|
||||
const [level, setLevel] = useState(1);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const gameLoopRef = useRef<number | null>(null);
|
||||
const keysPressed = useRef<{ [key: string]: boolean }>({});
|
||||
|
||||
// Keyboard event handlers
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
keysPressed.current[e.key.toLowerCase()] = true;
|
||||
|
||||
// Space to start/pause/resume
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
if (gameState === "idle" || gameState === "gameover") {
|
||||
setGameState("playing");
|
||||
} else if (gameState === "playing") {
|
||||
setGameState("paused");
|
||||
} else if (gameState === "paused") {
|
||||
setGameState("playing");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
keysPressed.current[e.key.toLowerCase()] = false;
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, [gameState]);
|
||||
|
||||
// Mouse event handler
|
||||
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Simple click-based interaction
|
||||
if (gameState === "playing") {
|
||||
setScore((prev) => prev + 10);
|
||||
}
|
||||
};
|
||||
|
||||
// Game loop
|
||||
useEffect(() => {
|
||||
if (gameState !== "playing") return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationId: number;
|
||||
|
||||
const gameLoop = () => {
|
||||
// Clear canvas
|
||||
ctx.fillStyle = "rgba(15, 23, 42, 0.8)";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw game elements
|
||||
ctx.fillStyle = "#00ff88";
|
||||
ctx.font = "24px Arial";
|
||||
ctx.fillText(`Score: ${score}`, 20, 40);
|
||||
ctx.fillText(`Level: ${level}`, 20, 80);
|
||||
|
||||
// Draw controls hint
|
||||
if (gameState === "playing") {
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
|
||||
ctx.font = "14px Arial";
|
||||
ctx.fillText("Arrow keys: Move | Space: Pause | Click: Action", 20, canvas.height - 20);
|
||||
}
|
||||
|
||||
// Update score every frame
|
||||
if (gameState === "playing") {
|
||||
setScore((prev) => prev + 1);
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
|
||||
return () => cancelAnimationFrame(animationId);
|
||||
}, [gameState, score, level]);
|
||||
|
||||
const handlePauseResume = () => {
|
||||
if (gameState === "playing") {
|
||||
setGameState("paused");
|
||||
} else if (gameState === "paused") {
|
||||
setGameState("playing");
|
||||
}
|
||||
};
|
||||
|
||||
const handleGameOver = () => {
|
||||
setGameState("gameover");
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setScore(0);
|
||||
setLevel(1);
|
||||
setGameState("idle");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-4 p-4 bg-slate-900 rounded-lg">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={800}
|
||||
height={400}
|
||||
onClick={handleCanvasClick}
|
||||
className="w-full border-2 border-green-400 bg-slate-800 rounded cursor-pointer"
|
||||
/>
|
||||
<div className="flex gap-2 justify-center flex-wrap">
|
||||
{gameState === "idle" && (
|
||||
<button
|
||||
onClick={() => setGameState("playing")}
|
||||
className="px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition"
|
||||
>
|
||||
Start Game
|
||||
</button>
|
||||
)}
|
||||
{gameState === "playing" && (
|
||||
<button
|
||||
onClick={handlePauseResume}
|
||||
className="px-6 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600 transition"
|
||||
>
|
||||
Pause (Space)
|
||||
</button>
|
||||
)}
|
||||
{gameState === "paused" && (
|
||||
<button
|
||||
onClick={handlePauseResume}
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
||||
>
|
||||
Resume (Space)
|
||||
</button>
|
||||
)}
|
||||
{(gameState === "playing" || gameState === "paused") && (
|
||||
<button
|
||||
onClick={handleGameOver}
|
||||
className="px-6 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition"
|
||||
>
|
||||
End Game
|
||||
</button>
|
||||
)}
|
||||
{gameState === "gameover" && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-6 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 transition"
|
||||
>
|
||||
Play Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center text-sm text-gray-400">
|
||||
{gameState === "idle" && "Press Space or click Start to begin"}
|
||||
{gameState === "playing" && "Game Running - Press Space to pause"}
|
||||
{gameState === "paused" && "Game Paused - Press Space to resume"}
|
||||
{gameState === "gameover" && `Game Over! Final Score: ${score}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
const navItems = [
|
||||
@@ -111,18 +297,15 @@ export default function HomePage() {
|
||||
{
|
||||
id: "featured-1", brand: "PlayHub", name: "Neon Blasters Pro", price: "FREE", rating: 5,
|
||||
reviewCount: "12.5k", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-vibrant-action-game-thumbnail-with-exp-1773147106304-b4d00265.jpg", imageAlt: "Neon Blasters Pro action game"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-vibrant-action-game-thumbnail-with-exp-1773147106304-b4d00265.jpg", imageAlt: "Neon Blasters Pro action game"},
|
||||
{
|
||||
id: "featured-2", brand: "PlayHub", name: "Speed Zone Racing", price: "FREE", rating: 5,
|
||||
reviewCount: "9.8k", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-racing-game-screenshot-showing-high-sp-1773147106485-71fc80aa.png", imageAlt: "Speed Zone Racing game"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-racing-game-screenshot-showing-high-sp-1773147106485-71fc80aa.png", imageAlt: "Speed Zone Racing game"},
|
||||
{
|
||||
id: "featured-3", brand: "PlayHub", name: "Puzzle Master", price: "FREE", rating: 5,
|
||||
reviewCount: "15.2k", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-puzzle-game-interface-with-colorful-ma-1773147105895-ad36efba.jpg", imageAlt: "Puzzle Master game"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-puzzle-game-interface-with-colorful-ma-1773147105895-ad36efba.jpg", imageAlt: "Puzzle Master game"},
|
||||
]}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
animationType="slide-up"
|
||||
@@ -141,43 +324,37 @@ export default function HomePage() {
|
||||
icon: Zap,
|
||||
title: "Action", description:
|
||||
"High-octane gaming with intense combat and explosive gameplay. Perfect for adrenaline seekers.", button: {
|
||||
text: "Play Action Games", href: "/games"
|
||||
},
|
||||
text: "Play Action Games", href: "/games"},
|
||||
},
|
||||
{
|
||||
icon: Gauge,
|
||||
title: "Racing", description:
|
||||
"Speed through tracks with realistic vehicles and competitive racing experiences.", button: {
|
||||
text: "Play Racing Games", href: "/games"
|
||||
},
|
||||
text: "Play Racing Games", href: "/games"},
|
||||
},
|
||||
{
|
||||
icon: Brain,
|
||||
title: "Puzzle", description:
|
||||
"Challenge your mind with logic puzzles, match-3 games, and brain teasers.", button: {
|
||||
text: "Play Puzzle Games", href: "/games"
|
||||
},
|
||||
text: "Play Puzzle Games", href: "/games"},
|
||||
},
|
||||
{
|
||||
icon: Compass,
|
||||
title: "Adventure", description:
|
||||
"Embark on epic quests and explore immersive worlds filled with mystery and wonder.", button: {
|
||||
text: "Play Adventure Games", href: "/games"
|
||||
},
|
||||
text: "Play Adventure Games", href: "/games"},
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
title: "Sports", description:
|
||||
"Compete in various sports simulations from football to basketball to esports.", button: {
|
||||
text: "Play Sports Games", href: "/games"
|
||||
},
|
||||
text: "Play Sports Games", href: "/games"},
|
||||
},
|
||||
{
|
||||
icon: Gamepad2,
|
||||
title: "Arcade", description:
|
||||
"Classic arcade thrills with retro gameplay, pixel art, and nostalgic gaming joy.", button: {
|
||||
text: "Play Arcade Games", href: "/games"
|
||||
},
|
||||
text: "Play Arcade Games", href: "/games"},
|
||||
},
|
||||
]}
|
||||
animationType="slide-up"
|
||||
@@ -187,38 +364,23 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
<div id="popular-games" data-section="popular-games">
|
||||
<ProductCardTwo
|
||||
title="Popular Games This Week"
|
||||
description="Join millions of players enjoying these trending games right now. Played by over 50 million gamers worldwide."
|
||||
tag="Trending"
|
||||
products={[
|
||||
{
|
||||
id: "popular-1", brand: "PlayHub Studio", name: "Team Legends Battle", price: "FREE", rating: 4,
|
||||
reviewCount: "45.3k", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-popular-multiplayer-game-screenshot-fe-1773147108371-57fdc73e.png", imageAlt: "Team Legends Battle multiplayer game"
|
||||
},
|
||||
{
|
||||
id: "popular-2", brand: "PlayHub Studio", name: "Strategic Conquest", price: "FREE", rating: 5,
|
||||
reviewCount: "32.1k", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-strategy-game-interface-showing-game-b-1773147107363-605d5cf4.png", imageAlt: "Strategic Conquest game"
|
||||
},
|
||||
{
|
||||
id: "popular-3", brand: "PlayHub Studio", name: "Precision Fire Elite", price: "FREE", rating: 4,
|
||||
reviewCount: "38.7k", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-shooting-game-screenshot-with-first-pe-1773147107276-7261d147.png", imageAlt: "Precision Fire Elite shooter game"
|
||||
},
|
||||
{
|
||||
id: "popular-4", brand: "PlayHub Studio", name: "Zen Garden Match", price: "FREE", rating: 5,
|
||||
reviewCount: "28.4k", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-casual-mobile-game-screenshot-with-sim-1773147107159-09cfc4e7.png", imageAlt: "Zen Garden Match casual game"
|
||||
},
|
||||
]}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
animationType="slide-up"
|
||||
textboxLayout="default"
|
||||
useInvertedBackground={false}
|
||||
carouselMode="auto"
|
||||
/>
|
||||
<div className="w-full max-w-7xl mx-auto px-4 py-16">
|
||||
<h2 className="text-3xl font-bold mb-4">Popular Games This Week - Play Now</h2>
|
||||
<p className="text-gray-400 mb-8">Experience interactive gameplay with keyboard/mouse controls. Press Space to pause/resume, click to interact.</p>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<GameContainer gameId="game-1" />
|
||||
<GameContainer gameId="game-2" />
|
||||
</div>
|
||||
<div className="mt-8 p-4 bg-slate-800 rounded text-sm text-gray-300">
|
||||
<p className="font-semibold mb-2">Game Controls:</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li><span className="text-green-400">Arrow Keys</span> - Move player/control direction</li>
|
||||
<li><span className="text-green-400">Space Bar</span> - Start/Pause/Resume game</li>
|
||||
<li><span className="text-green-400">Mouse Click</span> - Interact with game elements</li>
|
||||
<li><span className="text-green-400">Play Again</span> - Reset and start over after game over</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="testimonials" data-section="testimonials">
|
||||
@@ -230,33 +392,27 @@ export default function HomePage() {
|
||||
{
|
||||
id: "test-1", title: "Best free gaming platform ever", quote:
|
||||
"PlayHub changed my gaming life! I can play so many games without installing anything. The variety and quality are incredible. Highly recommend!", name: "Alex Chen", role: "Gaming Enthusiast", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-professional-gaming-enthusiast-portrai-1773147108470-29bb2604.png", imageAlt: "Alex Chen testimonial"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-professional-gaming-enthusiast-portrai-1773147108470-29bb2604.png", imageAlt: "Alex Chen testimonial"},
|
||||
{
|
||||
id: "test-2", title: "Perfect for casual gaming", quote:
|
||||
"I love how easy it is to jump into a game on PlayHub. Whether I have 5 minutes or an hour, there's something perfect for me. Best discovery ever!", name: "Sarah Williams", role: "Casual Player", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-young-casual-gamer-portrait-smiling-ho-1773147107760-3f1647bf.jpg", imageAlt: "Sarah Williams testimonial"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-young-casual-gamer-portrait-smiling-ho-1773147107760-3f1647bf.jpg", imageAlt: "Sarah Williams testimonial"},
|
||||
{
|
||||
id: "test-3", title: "Serious gamer's paradise", quote:
|
||||
"The competitive games on PlayHub are insane! Great balance, smooth gameplay, and an amazing community. This is where I spend most of my gaming time.", name: "Marcus Johnson", role: "Competitive Player", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-competitive-gamer-portrait-intense-foc-1773147108094-9100416c.jpg", imageAlt: "Marcus Johnson testimonial"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-competitive-gamer-portrait-intense-foc-1773147108094-9100416c.jpg", imageAlt: "Marcus Johnson testimonial"},
|
||||
{
|
||||
id: "test-4", title: "Something for everyone", quote:
|
||||
"My whole family plays different games on PlayHub. From puzzle games to action, everyone finds what they love. It's a game-changer for households!", name: "Emma Rodriguez", role: "Family Gamer", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-diverse-gamer-portrait-friendly-approa-1773147107832-c70ea8c8.png", imageAlt: "Emma Rodriguez testimonial"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-diverse-gamer-portrait-friendly-approa-1773147107832-c70ea8c8.png", imageAlt: "Emma Rodriguez testimonial"},
|
||||
{
|
||||
id: "test-5", title: "Mobile gaming done right", quote:
|
||||
"I play on my phone during commutes. PlayHub's mobile experience is smooth, responsive, and packed with amazing games. No lags, pure gaming joy!", name: "David Park", role: "Mobile Gamer", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-mobile-gamer-portrait-casual-comfortab-1773147108245-b78865a8.jpg", imageAlt: "David Park testimonial"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-mobile-gamer-portrait-casual-comfortab-1773147108245-b78865a8.jpg", imageAlt: "David Park testimonial"},
|
||||
{
|
||||
id: "test-6", title: "The ultimate gaming hub", quote:
|
||||
"As a hardcore gamer, I was skeptical, but PlayHub delivers. The game selection is vast, servers are reliable, and the community is fantastic. Respect!", name: "Lisa Anderson", role: "Hardcore Gamer", imageSrc:
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-hardcore-gamer-portrait-with-gaming-se-1773147108492-803fc87c.jpg", imageAlt: "Lisa Anderson testimonial"
|
||||
},
|
||||
"https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AkizvW9pyMSIEgho4Reud2UqvC/a-hardcore-gamer-portrait-with-gaming-se-1773147108492-803fc87c.jpg", imageAlt: "Lisa Anderson testimonial"},
|
||||
]}
|
||||
textboxLayout="default"
|
||||
useInvertedBackground={false}
|
||||
@@ -270,17 +426,13 @@ export default function HomePage() {
|
||||
tag="Global Platform"
|
||||
metrics={[
|
||||
{
|
||||
id: "metric-1", value: "100+", description: "Free Games Available"
|
||||
},
|
||||
id: "metric-1", value: "100+", description: "Free Games Available"},
|
||||
{
|
||||
id: "metric-2", value: "50M+", description: "Active Players Monthly"
|
||||
},
|
||||
id: "metric-2", value: "50M+", description: "Active Players Monthly"},
|
||||
{
|
||||
id: "metric-3", value: "150K+", description: "Games Played Daily"
|
||||
},
|
||||
id: "metric-3", value: "150K+", description: "Games Played Daily"},
|
||||
{
|
||||
id: "metric-4", value: "98%", description: "Player Satisfaction"
|
||||
},
|
||||
id: "metric-4", value: "98%", description: "Player Satisfaction"},
|
||||
]}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
animationType="slide-up"
|
||||
|
||||
102
src/components/game/GameCanvas.tsx
Normal file
102
src/components/game/GameCanvas.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { GameEngine, InputManager, SceneManager, GameConfig } from "@/utils/gameEngine";
|
||||
|
||||
export interface GameCanvasProps {
|
||||
config: GameConfig;
|
||||
onEngineReady?: (engine: GameEngine, inputManager: InputManager, sceneManager: SceneManager) => void;
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable game canvas component with engine initialization
|
||||
* Handles canvas rendering, game loop, and input management
|
||||
*/
|
||||
export const GameCanvas = React.forwardRef<HTMLCanvasElement, GameCanvasProps>(
|
||||
({ config, onEngineReady, className = "", ariaLabel = "Game canvas" }, ref) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const engineRef = useRef<GameEngine | null>(null);
|
||||
const inputManagerRef = useRef<InputManager | null>(null);
|
||||
const sceneManagerRef = useRef<SceneManager | null>(null);
|
||||
|
||||
// Expose canvas ref
|
||||
useEffect(() => {
|
||||
if (ref) {
|
||||
if (typeof ref === "function") {
|
||||
ref(canvasRef.current);
|
||||
} else {
|
||||
ref.current = canvasRef.current;
|
||||
}
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
// Initialize game engine
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
try {
|
||||
// Create engine
|
||||
const engine = new GameEngine(config);
|
||||
const initialized = engine.initialize(canvasRef.current);
|
||||
|
||||
if (!initialized) {
|
||||
console.error("Failed to initialize game engine");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create input and scene managers
|
||||
const inputManager = new InputManager(canvasRef.current);
|
||||
const sceneManager = new SceneManager();
|
||||
|
||||
// Store references
|
||||
engineRef.current = engine;
|
||||
inputManagerRef.current = inputManager;
|
||||
sceneManagerRef.current = sceneManager;
|
||||
|
||||
// Initialize scene
|
||||
sceneManager.createScene("main");
|
||||
sceneManager.setActiveScene("main");
|
||||
|
||||
// Call callback if provided
|
||||
if (onEngineReady) {
|
||||
onEngineReady(engine, inputManager, sceneManager);
|
||||
}
|
||||
|
||||
// Start game loop
|
||||
engine.start();
|
||||
|
||||
// Handle window resize
|
||||
const handleResize = () => {
|
||||
if (canvasRef.current) {
|
||||
engine.resize(canvasRef.current.offsetWidth, canvasRef.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
engine.stop();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Game engine initialization error:", error);
|
||||
}
|
||||
}, [config, onEngineReady]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`w-full h-full bg-black rounded-lg shadow-lg ${className}`}
|
||||
aria-label={ariaLabel}
|
||||
role="img"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GameCanvas.displayName = "GameCanvas";
|
||||
|
||||
export default GameCanvas;
|
||||
160
src/components/game/GameComponent.tsx
Normal file
160
src/components/game/GameComponent.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { GameEngine, InputManager, SceneManager, GameObject, GameConfig } from "@/utils/gameEngine";
|
||||
import GameCanvas from "./GameCanvas";
|
||||
|
||||
export interface GameComponentProps {
|
||||
gameConfig: GameConfig;
|
||||
onInitialize?: (engine: GameEngine, input: InputManager, scene: SceneManager) => void;
|
||||
onFrame?: (engine: GameEngine, input: InputManager) => void;
|
||||
onCleanup?: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
showStats?: boolean;
|
||||
className?: string;
|
||||
canvasClassName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable game component wrapper
|
||||
* Provides game loop integration, state management, and UI overlay
|
||||
*/
|
||||
export const GameComponent: React.FC<GameComponentProps> = ({
|
||||
gameConfig,
|
||||
onInitialize,
|
||||
onFrame,
|
||||
onCleanup,
|
||||
title,
|
||||
description,
|
||||
showStats = false,
|
||||
className = "", canvasClassName = ""}) => {
|
||||
const [stats, setStats] = useState({
|
||||
fps: 0,
|
||||
deltaTime: 0,
|
||||
elapsedTime: 0,
|
||||
frameCount: 0,
|
||||
});
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const engineRef = React.useRef<GameEngine | null>(null);
|
||||
const inputRef = React.useRef<InputManager | null>(null);
|
||||
const sceneRef = React.useRef<SceneManager | null>(null);
|
||||
const statsIntervalRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleEngineReady = (engine: GameEngine, input: InputManager, scene: SceneManager) => {
|
||||
engineRef.current = engine;
|
||||
inputRef.current = input;
|
||||
sceneRef.current = scene;
|
||||
|
||||
// Setup game update loop
|
||||
engine.onUpdate((deltaTime: number) => {
|
||||
if (onFrame && inputRef.current) {
|
||||
onFrame(engine, inputRef.current);
|
||||
}
|
||||
|
||||
// Update scene objects
|
||||
const objects = scene.getActiveObjects();
|
||||
objects.forEach((obj) => {
|
||||
if (obj.active) {
|
||||
obj.update(deltaTime);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Setup rendering
|
||||
engine.onRender((ctx) => {
|
||||
// Render scene objects
|
||||
const objects = scene.getActiveObjects();
|
||||
objects.forEach((obj) => {
|
||||
if (obj.visible) {
|
||||
obj.render(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Call user initialization callback
|
||||
if (onInitialize) {
|
||||
onInitialize(engine, input, scene);
|
||||
}
|
||||
|
||||
// Setup stats updates
|
||||
if (showStats) {
|
||||
statsIntervalRef.current = setInterval(() => {
|
||||
setStats({
|
||||
fps: engine.state.fps,
|
||||
deltaTime: engine.state.deltaTime,
|
||||
elapsedTime: engine.state.elapsedTime,
|
||||
frameCount: engine.state.frameCount,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePause = () => {
|
||||
if (!engineRef.current) return;
|
||||
|
||||
if (isPaused) {
|
||||
engineRef.current.resume();
|
||||
setIsPaused(false);
|
||||
} else {
|
||||
engineRef.current.pause();
|
||||
setIsPaused(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (statsIntervalRef.current) {
|
||||
clearInterval(statsIntervalRef.current);
|
||||
}
|
||||
if (onCleanup) {
|
||||
onCleanup();
|
||||
}
|
||||
};
|
||||
}, [onCleanup]);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-full bg-gray-900 rounded-lg overflow-hidden ${className}`}>
|
||||
<GameCanvas
|
||||
config={gameConfig}
|
||||
onEngineReady={handleEngineReady}
|
||||
className={canvasClassName}
|
||||
/>
|
||||
|
||||
{/* Game overlay UI */}
|
||||
{(title || description || showStats || true) && (
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0 pointer-events-none flex flex-col justify-between p-4">
|
||||
{/* Header */}
|
||||
{(title || description) && (
|
||||
<div className="pointer-events-auto">
|
||||
{title && <h2 className="text-white text-lg font-bold">{title}</h2>}
|
||||
{description && <p className="text-gray-300 text-sm">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex gap-2 pointer-events-auto">
|
||||
<button
|
||||
onClick={togglePause}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition"
|
||||
aria-label={isPaused ? "Resume game" : "Pause game"}
|
||||
>
|
||||
{isPaused ? "Resume" : "Pause"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats Display */}
|
||||
{showStats && (
|
||||
<div className="pointer-events-auto text-right text-xs text-gray-400 font-mono space-y-1">
|
||||
<div>FPS: {stats.fps}</div>
|
||||
<div>Frame: {stats.frameCount}</div>
|
||||
<div>Time: {stats.elapsedTime.toFixed(2)}s</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameComponent;
|
||||
388
src/utils/gameEngine.ts
Normal file
388
src/utils/gameEngine.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* Game Engine Infrastructure
|
||||
* Provides core game loop, state management, and canvas/WebGL rendering utilities
|
||||
*/
|
||||
|
||||
export interface GameState {
|
||||
isPaused: boolean;
|
||||
isRunning: boolean;
|
||||
deltaTime: number;
|
||||
elapsedTime: number;
|
||||
frameCount: number;
|
||||
fps: number;
|
||||
}
|
||||
|
||||
export interface GameConfig {
|
||||
width: number;
|
||||
height: number;
|
||||
fps?: number;
|
||||
useWebGL?: boolean;
|
||||
}
|
||||
|
||||
export class GameEngine {
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private ctx: CanvasRenderingContext2D | WebGLRenderingContext | null = null;
|
||||
private isWebGL: boolean = false;
|
||||
private animationFrameId: number | null = null;
|
||||
private lastFrameTime: number = 0;
|
||||
private frameCount: number = 0;
|
||||
private targetFPS: number = 60;
|
||||
private accumulatedTime: number = 0;
|
||||
|
||||
state: GameState = {
|
||||
isPaused: false,
|
||||
isRunning: false,
|
||||
deltaTime: 0,
|
||||
elapsedTime: 0,
|
||||
frameCount: 0,
|
||||
fps: 0,
|
||||
};
|
||||
|
||||
private updateCallbacks: Array<(deltaTime: number) => void> = [];
|
||||
private renderCallbacks: Array<(ctx: CanvasRenderingContext2D | WebGLRenderingContext) => void> = [];
|
||||
|
||||
constructor(config: GameConfig) {
|
||||
this.targetFPS = config.fps || 60;
|
||||
this.isWebGL = config.useWebGL || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the game engine with a canvas element
|
||||
*/
|
||||
initialize(canvas: HTMLCanvasElement): boolean {
|
||||
if (!canvas) return false;
|
||||
|
||||
this.canvas = canvas;
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
|
||||
if (this.isWebGL) {
|
||||
this.ctx = canvas.getContext('webgl') || canvas.getContext('webgl2');
|
||||
if (!this.ctx) {
|
||||
console.warn('WebGL not supported, falling back to 2D');
|
||||
this.ctx = canvas.getContext('2d');
|
||||
this.isWebGL = false;
|
||||
}
|
||||
} else {
|
||||
this.ctx = canvas.getContext('2d');
|
||||
}
|
||||
|
||||
return this.ctx !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an update callback to be called each frame
|
||||
*/
|
||||
onUpdate(callback: (deltaTime: number) => void): void {
|
||||
this.updateCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a render callback to be called each frame
|
||||
*/
|
||||
onRender(callback: (ctx: CanvasRenderingContext2D | WebGLRenderingContext) => void): void {
|
||||
this.renderCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game loop
|
||||
*/
|
||||
start(): void {
|
||||
if (this.state.isRunning) return;
|
||||
|
||||
this.state.isRunning = true;
|
||||
this.state.isPaused = false;
|
||||
this.lastFrameTime = performance.now();
|
||||
this.accumulatedTime = 0;
|
||||
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the game
|
||||
*/
|
||||
pause(): void {
|
||||
this.state.isPaused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the game
|
||||
*/
|
||||
resume(): void {
|
||||
this.state.isPaused = false;
|
||||
this.lastFrameTime = performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the game loop
|
||||
*/
|
||||
stop(): void {
|
||||
this.state.isRunning = false;
|
||||
this.state.isPaused = false;
|
||||
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main game loop using requestAnimationFrame
|
||||
*/
|
||||
private gameLoop = (): void => {
|
||||
if (!this.state.isRunning) return;
|
||||
|
||||
const currentTime = performance.now();
|
||||
const frameDeltaTime = (currentTime - this.lastFrameTime) / 1000;
|
||||
this.lastFrameTime = currentTime;
|
||||
|
||||
if (!this.state.isPaused) {
|
||||
this.accumulatedTime += frameDeltaTime;
|
||||
const frameTime = 1 / this.targetFPS;
|
||||
|
||||
// Update game state
|
||||
while (this.accumulatedTime >= frameTime) {
|
||||
this.state.deltaTime = frameTime;
|
||||
this.state.elapsedTime += frameTime;
|
||||
this.state.frameCount++;
|
||||
|
||||
// Call update callbacks
|
||||
this.updateCallbacks.forEach(callback => callback(frameTime));
|
||||
|
||||
this.accumulatedTime -= frameTime;
|
||||
}
|
||||
|
||||
// Calculate FPS
|
||||
this.frameCount++;
|
||||
if (currentTime - (this.lastFrameTime - frameDeltaTime) >= 1000) {
|
||||
this.state.fps = this.frameCount;
|
||||
this.frameCount = 0;
|
||||
}
|
||||
|
||||
// Clear canvas
|
||||
this.clearCanvas();
|
||||
|
||||
// Call render callbacks
|
||||
if (this.ctx) {
|
||||
this.renderCallbacks.forEach(callback => callback(this.ctx!));
|
||||
}
|
||||
}
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(this.gameLoop);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the canvas
|
||||
*/
|
||||
private clearCanvas(): void {
|
||||
if (!this.canvas || !this.ctx) return;
|
||||
|
||||
if (this.isWebGL) {
|
||||
const gl = this.ctx as WebGLRenderingContext;
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||
} else {
|
||||
const ctx = this.ctx as CanvasRenderingContext2D;
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canvas dimensions
|
||||
*/
|
||||
getDimensions(): { width: number; height: number } {
|
||||
return {
|
||||
width: this.canvas?.width || 0,
|
||||
height: this.canvas?.height || 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize canvas
|
||||
*/
|
||||
resize(width: number, height: number): void {
|
||||
if (!this.canvas) return;
|
||||
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for reusable game components
|
||||
*/
|
||||
export class GameObject {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
width: number = 0;
|
||||
height: number = 0;
|
||||
rotation: number = 0;
|
||||
scaleX: number = 1;
|
||||
scaleY: number = 1;
|
||||
visible: boolean = true;
|
||||
active: boolean = true;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, width: number = 0, height: number = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game object state
|
||||
*/
|
||||
update(deltaTime: number): void {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
/**
|
||||
* Render game object
|
||||
*/
|
||||
render(ctx: CanvasRenderingContext2D | WebGLRenderingContext): void {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this object collides with another
|
||||
*/
|
||||
collidsWith(other: GameObject): boolean {
|
||||
return (
|
||||
this.x < other.x + other.width &&
|
||||
this.x + this.width > other.x &&
|
||||
this.y < other.y + other.height &&
|
||||
this.y + this.height > other.y
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bounds
|
||||
*/
|
||||
getBounds(): { x: number; y: number; width: number; height: number } {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input manager for handling keyboard and mouse input
|
||||
*/
|
||||
export class InputManager {
|
||||
private keysPressed: Map<string, boolean> = new Map();
|
||||
private mousePosition: { x: number; y: number } = { x: 0, y: 0 };
|
||||
private mouseButtons: Map<number, boolean> = new Map();
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
this.attachEventListeners(canvas);
|
||||
}
|
||||
|
||||
private attachEventListeners(canvas: HTMLCanvasElement): void {
|
||||
// Keyboard events
|
||||
document.addEventListener('keydown', (e) => {
|
||||
this.keysPressed.set(e.key.toLowerCase(), true);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
this.keysPressed.set(e.key.toLowerCase(), false);
|
||||
});
|
||||
|
||||
// Mouse events
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
this.mousePosition = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
};
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
this.mouseButtons.set(e.button, true);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', (e) => {
|
||||
this.mouseButtons.set(e.button, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key is pressed
|
||||
*/
|
||||
isKeyPressed(key: string): boolean {
|
||||
return this.keysPressed.get(key.toLowerCase()) || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mouse position
|
||||
*/
|
||||
getMousePosition(): { x: number; y: number } {
|
||||
return this.mousePosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a mouse button is pressed
|
||||
*/
|
||||
isMouseButtonPressed(button: number = 0): boolean {
|
||||
return this.mouseButtons.get(button) || false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene manager for managing game scenes
|
||||
*/
|
||||
export class SceneManager {
|
||||
private scenes: Map<string, GameObject[]> = new Map();
|
||||
private activeScene: string | null = null;
|
||||
|
||||
/**
|
||||
* Create a new scene
|
||||
*/
|
||||
createScene(name: string): void {
|
||||
this.scenes.set(name, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set active scene
|
||||
*/
|
||||
setActiveScene(name: string): boolean {
|
||||
if (this.scenes.has(name)) {
|
||||
this.activeScene = name;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add object to active scene
|
||||
*/
|
||||
addObject(obj: GameObject): void {
|
||||
if (this.activeScene) {
|
||||
const scene = this.scenes.get(this.activeScene);
|
||||
if (scene) {
|
||||
scene.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active objects
|
||||
*/
|
||||
getActiveObjects(): GameObject[] {
|
||||
if (this.activeScene) {
|
||||
return this.scenes.get(this.activeScene) || [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear active scene
|
||||
*/
|
||||
clearActiveScene(): void {
|
||||
if (this.activeScene) {
|
||||
this.scenes.set(this.activeScene, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user