debounce와 throttle 함수는 모두 자바스크립트에서 이벤트 핸들링을 최적화하기 위해 사용되는 고차 함수입니다. 이 두 함수는 특정 이벤트가 발생할 때 너무 많은 함수 호출을 방지하고 성능을 향상시키기 위해 사용됩니다. 그러나 이들은 서로 다른 방식으로 동작합니다.
'use client';
import Link from 'next/link';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PokeBallImage from '@/assets/default ball.png';
import PokemonImage from '@/assets/pokemon2.png';
type ItemPos = {
x: number;
y: number;
w: number;
h: number;
};
export default function PokeBallGamePage() {
const [state, setState] = useState<'play' | 'pause' | 'stop'>('stop');
const [score, setScore] = useState(0);
const ref = useRef<HTMLCanvasElement>(null);
const pokemonRef = useRef<HTMLImageElement>(null);
const pokeBallRef = useRef<HTMLImageElement>(null);
const pokeBallSizeRef = useRef({ w: 50, h: 50 });
const posRef = useRef<{
pokeBalls: ItemPos[];
pokeBallAccel: number[];
pokemon: ItemPos;
}>({
pokeBalls: [],
pokeBallAccel: [],
pokemon: { x: 0, y: 0, w: 0, h: 0 }
});
const keyRef = useRef({
isLeft: false,
isRight: false
});
const W = 500;
const H = 600;
const VELOCITY = {
pokemon: {
left: 6,
right: 6
},
pokeBallAccel: 0.03
};
const CREATE_POKEBALL_TIME = 500;
const AVOID_POKEBALL_SCORE = 10;
const drawImage = useCallback((ctx: CanvasRenderingContext2D, img: HTMLImageElement, { x, y, w, h }: ItemPos) => {
const maxWidth = 50;
const maxHeight = 50;
const aspectRatio = img.width / img.height;
if (w > maxWidth || h > maxHeight) {
if (aspectRatio > 1) {
w = maxWidth;
h = maxWidth / aspectRatio;
} else {
w = maxHeight * aspectRatio;
h = maxHeight;
}
}
ctx.drawImage(img, x, y, w, h);
}, []);
const loadImage = useCallback(
(src: string) =>
new Promise<HTMLImageElement>((resolve) => {
const img = new Image();
img.src = src;
img.onload = () => resolve(img);
}),
[]
);
const blockOverflowPos = useCallback(
(pos: ItemPos) => {
pos.x = pos.x + pos.w >= W ? W - pos.w : pos.x < 0 ? 0 : pos.x;
pos.y = pos.y + pos.h >= H ? H - pos.h : pos.y < 0 ? 0 : pos.y;
},
[W, H]
);
const updatePokemonPos = useCallback(
(pokemonPos: ItemPos) => {
const key = keyRef.current;
if (key.isLeft) pokemonPos.x -= VELOCITY.pokemon.left;
if (key.isRight) pokemonPos.x += VELOCITY.pokemon.right;
blockOverflowPos(pokemonPos);
},
[blockOverflowPos]
);
const createPokeBall = useCallback(() => {
if (!pokeBallRef.current) return;
const size = pokeBallSizeRef.current;
posRef.current.pokeBalls.push({
x: Math.random() * (W - size.w),
y: -size.h,
...size
});
posRef.current.pokeBallAccel.push(1);
}, [W]);
const updatePokeBallPos = useCallback((pokeBallPos: ItemPos, index: number) => {
const y = pokeBallPos.y;
const accel = posRef.current.pokeBallAccel[index];
posRef.current.pokeBallAccel[index] = accel + accel * VELOCITY.pokeBallAccel;
pokeBallPos.y = y + accel;
}, []);
const deletePokeBall = useCallback((index: number) => {
posRef.current.pokeBalls.splice(index, 1);
posRef.current.pokeBallAccel.splice(index, 1);
setScore((prevScore) => prevScore + AVOID_POKEBALL_SCORE);
createPokeBall();
}, []);
const catchPokeBall = useCallback(
(pokeBallPos: ItemPos, index: number) => {
const pokemonPos = posRef.current.pokemon;
if (
pokemonPos.x + pokemonPos.w >= pokeBallPos.x &&
pokemonPos.x <= pokeBallPos.x + pokeBallPos.w &&
pokemonPos.y + pokemonPos.h >= pokeBallPos.y &&
pokemonPos.y <= pokeBallPos.y + pokeBallPos.h
) {
alert(`점수: ${score}`);
setState('stop');
}
},
[score]
);
const initialGame = useCallback(
(ctx: CanvasRenderingContext2D) => {
ctx.clearRect(0, 0, W, H);
const { w, h } = posRef.current.pokemon;
posRef.current.pokeBallAccel = [];
posRef.current.pokeBalls = [];
posRef.current.pokemon = {
x: W / 2 - w / 2,
y: H - h,
w,
h
};
keyRef.current.isLeft = false;
keyRef.current.isRight = false;
setScore(0);
},
[W, H]
);
useEffect(() => {
const cvs = ref.current;
const ctx = cvs?.getContext('2d');
state === 'stop' && ctx && initialGame(ctx);
if (!cvs || !ctx || state !== 'play') return;
!pokemonRef.current &&
loadImage(PokemonImage.src).then((img) => {
(pokemonRef as any).current = img;
const w = 50;
const h = 50;
posRef.current.pokemon = {
x: W / 2 - w / 2,
y: H - h,
w,
h
};
});
!pokeBallRef.current &&
loadImage(PokeBallImage.src).then((img) => {
(pokeBallRef as any).current = img;
pokeBallSizeRef.current.w = 50;
pokeBallSizeRef.current.h = 50;
});
let timer: number | undefined;
let rafTimer: number | undefined;
const pos = posRef.current;
const animate = () => {
const pokemon = pokemonRef.current;
const pokeBall = pokeBallRef.current;
ctx.clearRect(0, 0, W, H);
if (pokemon) {
updatePokemonPos(pos.pokemon);
drawImage(ctx, pokemon, pos.pokemon);
}
if (pokeBall) {
pos.pokeBalls.forEach((pokeBallPos, index) => {
updatePokeBallPos(pokeBallPos, index);
drawImage(ctx, pokeBall, pokeBallPos);
});
pos.pokeBalls.forEach((pokeBallPos, index) => {
if (pokeBallPos.y >= H) {
deletePokeBall(index);
} else {
catchPokeBall(pokeBallPos, index);
}
});
}
rafTimer = requestAnimationFrame(animate);
};
rafTimer = requestAnimationFrame(animate);
timer = window.setInterval(createPokeBall, CREATE_POKEBALL_TIME);
const onKeyDown = (e: KeyboardEvent) => {
const key = e.key.toLowerCase();
keyRef.current.isLeft = key === 'a' || key === 'arrowleft';
keyRef.current.isRight = key === 'd' || key === 'arrowright';
};
const onKeyUp = () => {
keyRef.current.isLeft = false;
keyRef.current.isRight = false;
};
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
timer && window.clearInterval(timer);
timer = undefined;
rafTimer && cancelAnimationFrame(rafTimer);
rafTimer = undefined;
};
}, [
drawImage,
loadImage,
updatePokeBallPos,
createPokeBall,
updatePokeBallPos,
deletePokeBall,
catchPokeBall,
state,
initialGame
]);
return (
<div className="flex min-h-screen items-center justify-center">
<div className="h-[800px] w-[600px] bg-white p-4">
<div className="mx-auto my-3 text-center">
<div className="flex justify-center space-x-5">
<button type="button" onClick={() => setState('pause')}>
PAUSE
</button>
<button type="button" onClick={() => setState('play')}>
PLAY
</button>
<button type="button" onClick={() => setState('stop')}>
STOP
</button>
<Link className="text-red-500" href="/game">
GoLobby
</Link>
</div>
<p className="mt-2">현재 점수: {score}</p>
</div>
<canvas className="mx-auto block border border-black" ref={ref} width={W} height={H} />
</div>
</div>
);
}
현재 로직상 포켓볼 속도와 포켓볼 생성주기에따라 포켓볼의 갯수가 결정됨.