본문 바로가기

카테고리 없음

TeamProject_PokéGotchi - 4일차

키를 꾹 누르면서 입력 시 가속도가 붙듯이 한번에 많이 움직이는 문제 발생

 

기존 코드

  const handleKeyDown = (event: KeyboardEvent) => {
    switch (event.key) {
      case 'a':
        moveSelectedPokemon(-20, 0);
        break;
      case 'd':
        moveSelectedPokemon(20, 0);
        break;
      case ' ':
        moveSelectedPokemon(0, -50);
        setTimeout(() => moveSelectedPokemon(0, 50), 500);
        break;
    }
  };

 

변경 코드

import _ from 'lodash';
 
  const debouncedMoveSelectedPokemon = useCallback(
    _.debounce((dx: number, dy: number) => {
      moveSelectedPokemon(dx, dy);
    }, 300),
    [selectedPokemon]
  );

  const throttledMoveSelectedPokemon = useCallback(
    _.throttle((dx: number, dy: number) => {
      moveSelectedPokemon(dx, dy);
    }, 500),
    [selectedPokemon]
  );

  const handleKeyDown = (event: KeyboardEvent) => {
    switch (event.key) {
      case 'a':
        throttledMoveSelectedPokemon(-30, 0);
        break;
      case 'd':
        throttledMoveSelectedPokemon(30, 0);
        break;
      case ' ':
        debouncedMoveSelectedPokemon(0, -50);
        setTimeout(() => debouncedMoveSelectedPokemon(0, 50), 500);
        break;
    }
  };

기존 코드는 키 입력 시 이동이 계속 입력 되었지만 lodash에 debounce함수를 이용하여 연속 입력 방지하여 해결

+ 빠르게 연타하여 입력 시 문제도 발생하여서 throttle함수도 활용

-> 좌우 움직임이 연타+꾹 누를시에 모두 대처가 되었으나 점프가 꾹 누를시엔 괜찮으나 연타에 아래로만 움직이는 문제 남음

 

 

debounce 와 throttle ( GPT 답변 인용 )

 

debounce와 throttle 함수는 모두 자바스크립트에서 이벤트 핸들링을 최적화하기 위해 사용되는 고차 함수입니다. 이 두 함수는 특정 이벤트가 발생할 때 너무 많은 함수 호출을 방지하고 성능을 향상시키기 위해 사용됩니다. 그러나 이들은 서로 다른 방식으로 동작합니다.

Debounce

설명

  • Debounce 함수는 연이어 호출되는 함수들 중 마지막 함수 호출만을 실행합니다. 특정 시간 동안 계속해서 호출될 경우, 그 시간 동안 함수 호출을 지연시키고, 그 시간 동안 함수 호출이 멈추면 마지막 호출만 실행됩니다.

장단점

  • 장점: 짧은 시간에 여러 번 발생하는 이벤트를 하나의 이벤트로 묶어 처리할 수 있습니다. 사용자가 이벤트를 멈출 때까지 기다리기 때문에, 정확한 처리가 가능합니다.
  • 단점: 연속된 이벤트가 발생할 때 이벤트 처리가 지연될 수 있습니다.

활용 방법

  • 사용 사례:
    • 검색 창에서 사용자가 입력을 멈춘 후에 검색 쿼리를 전송하는 경우
    • 창 크기 조정이 끝난 후에 레이아웃을 재계산하는 경우

Throttle

설명

  • Throttle 함수는 지정된 시간 간격마다 함수가 최대 한 번만 실행되도록 합니다. 연속된 호출이 있을 때 일정 시간 간격으로 함수를 실행하며, 그 시간 간격 내에 추가 호출이 발생해도 무시됩니다.

장단점

  • 장점: 연속된 이벤트가 발생할 때 주기적으로 함수를 실행하여, 이벤트의 빈도에 따라 함수 호출을 제한할 수 있습니다. 일정한 간격으로 처리가 되기 때문에 실시간 반응성이 필요할 때 유용합니다.
  • 단점: 지정된 시간 간격 동안 발생한 모든 이벤트를 처리하지 않고 일부만 처리합니다.

활용 방법

  • 사용 사례:
    • 스크롤 이벤트에서 일정 시간 간격으로 스크롤 위치를 체크하는 경우
    • 마우스 드래그 이벤트에서 일정 시간 간격으로 위치를 업데이트하는 경우

요약

  • Debounce
    • 연이어 발생하는 이벤트가 멈출 때까지 기다렸다가 마지막 이벤트만 처리
    • 검색 입력, 창 크기 조정 완료 후 작업 등에 유용
    • 사용 예시: 검색 입력 필드
  • Throttle
    • 연이어 발생하는 이벤트를 일정 간격으로 처리
    • 스크롤, 마우스 이동 등의 고빈도 이벤트에 유용
    • 사용 예시: 스크롤 위치 체크, 드래그 앤 드롭

 

포켓볼 피하기 게임

포켓볼 피하기 게임에서 난이도 설정에서 어려움이 있음

ex) 100점마다 포켓볼 갯수 , 떨어지는 속도 등의 차이 생성되게끔

 

실행 화면

 

현재 코드

'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>
  );
}

 

현재 로직상 포켓볼 속도와 포켓볼 생성주기에따라 포켓볼의 갯수가 결정됨.

이를 활용하여 난이도를 설정하면 유저 만족도가 올라갈 것으로 기대됨

 

난이도 조절 코드

  const [createPokeBallTime, setCreatePokeBallTime] = useState(1000);
  const [pokeBallAccel, setPokeBallAccel] = useState(0.03);
  useEffect(() => {
    if (score % 100 === 0 && score !== 0) {
      setCreatePokeBallTime((prevTime) => Math.max(prevTime - 20, 100));
    }
    if (score % 500 === 0 && score !== 0) {
      setPokeBallAccel((prevAccel) => prevAccel + 0.01);
    }
  }, [score]);

포켓볼 생성 및 속도와 관련된 변수를 상태 관리하여 점수에 따른 난이도 추가 세부적인 조정 요망

 

퀴즈 맞히기 게임

실행 화면

문제가 길어짐에따라 질문 크기가 더 큰 문제 + 번역 문제

번역을 위해 API를 찾아 보았으나

https://github.com/LibreTranslate/LibreTranslate

 

GitHub - LibreTranslate/LibreTranslate: Free and Open Source Machine Translation API. Self-hosted, offline capable and easy to s

Free and Open Source Machine Translation API. Self-hosted, offline capable and easy to setup. - LibreTranslate/LibreTranslate

github.com

https://portal.libretranslate.com/

 

LibreTranslate Portal - Get API Key

check Flat rate! Only $29 / month check Unlimited translations, up to 80 API calls / minute check Maximum privacy check E-mail support check 7 days money-back guarantee check Cancel anytime check Support LibreTranslate ❤ verified_user Buy

portal.libretranslate.com

무료 API지만 속도 제한으로인해 사용이 쉽지않고, 해결을 위한 키 발급은 유료이고

 

https://www.deepl.com/ko/pro#developer

 

DeepL Pro | 텍스트, Word 및 기타 문서를 안전하게 번역하세요

빠르고 정확하며 안전한 번역. 개인 및 팀 사용자용 DeepL Pro.

www.deepl.com

deepl에 경우에는 월 50만자 무료이긴하나, 할당량을 금새 사용해서 문제가 생길듯 하고

 

https://cloud.google.com/translate/pricing?hl=ko#cloud-translation-pricing

 

가격 책정  |  Cloud Translation  |  Google Cloud

Cloud Translation 가격 책정 검토

cloud.google.com

google번역 역시 90일 기간 + 글자제한 으로인해 쉽게 사용이 어려운듯 보임