고전 Snake 게임 만들기 - React II

10951 단어 reactgamedevtypescript
이 문서는 원래 mariokandut.com에 게시되었습니다.

이것은 자습서Create the classic Snake game - React의 두 번째 부분입니다. 첫 번째 부분은 에서 찾을 수 있습니다.

튜토리얼의 첫 번째 부분을 따랐다면 다음이 있어야 합니다.
  • 움직이는 제어 가능한 뱀
  • 경계 충돌 감지
  • 임의로 생성된 사과
  • 사과 충돌 감지(사과를 먹는 뱀)
  • 사과를 먹으면 뱀이 자란다

  • 누락된 부품과 기능을 계속 살펴보겠습니다.

    목차 - 파트 2

  • Fix collision detection for opposite direction keys
  • Styling Updates and Autofocus
  • Game Points
  • Game End

  • 1. 반대 방향 키에 대한 충돌 감지 수정

    Currently, the collision detection return true if we press the opposite key of the current direction the snake is moving. For example: The key ArrowRight is pressed, so the snake moves right, and then the key ArrowLeft is pressed. This would trigger a collision, which is a wrong behaviour. We have to fix this.

    An easy way to fix this, is to filter out keys which are in the opposite direction. Since we have a state for direction and coordinates for arrow keys, we can simply sum up the current direction and the arrow direction.

    The sum of x-coordinates for ArrowLeft and ArrowRight equal 0 and return a falsy value, hence this can be filtered.

      ArrowLeft: { x: -1, y: 0 },
      ArrowRight: { x: 1, y: 0 },
    

    Update the moveSnake with the following code:

    const moveSnake = (event: React.KeyboardEvent) => {
      const { key } = event;
      // Check if key is arrow key
      if (
        key === 'ArrowUp' ||
        key === 'ArrowDown' ||
        key === 'ArrowRight' ||
        key === 'ArrowLeft'
      ) {
        // disable backwards key, this means no collision when going right, and then pressing ArrowLeft
        if (
          direction.x + directions[key].x &&
          direction.y + directions[key].y
        ) {
          setDirection(directions[key]);
        }
      }
    };
    

    2. 스타일링 업데이트 및 자동 초점

    The styling of the game needs some improvement, and we have to add an overlay, if we lost the game, and autofocus. The styling will be made in the App.css , there are plenty of other ways to do styling in a React application. What styling method do you prefer? Leave a comment.

    The game wrapper should be autofocussed, after the start button is clicked. We have access to the focus() method, when we use the useRef hook.

    Add the wrapperRef and a state for isPlaying:

    // add wrapper ref and isPlaying flag for showing start button
    const wrapperRef = useRef<HTMLDivElement>(null);
    const [isPlaying, setIsPlaying] = useState<boolean>(false);
    

    Now we have to update the startGame and endGame function:

    // update startGame
    const startGame = () => {
        setIsPlaying(true);
        setSnake(SNAKE_START);
        setApple(APPLE_START);
        setDirection(DIRECTION_START);
        setSpeed(SPEED);
        setGameOver(false);
        wrapperRef.current?.focus();
      };
    
    // update endGame
    const endGame = () => {
        setIsPlaying(false);
        setSpeed(null);
        setGameOver(true);
      };
    

    Now we update the wrapper with some classNames and some condition for an overlay and the reference.

    // Update div with classes and flag for showing buttons, conditional styles
    return (
      <div className="wrapper">
        <div
          ref={wrapperRef}
          className="canvas"
          role="button"
          tabIndex={0}
          onKeyDown={(event: React.KeyboardEvent) => moveSnake(event)}
        >
          <canvas
            style={
              gameOver
                ? { border: '1px solid black', opacity: 0.5 }
                : { border: '1px solid black' }
            }
            ref={canvasRef}
            width={CANVAS_SIZE.x}
            height={CANVAS_SIZE.y}
          />
          {gameOver && <div className="game-over">Game Over</div>}
          {!isPlaying && (
            <button className="start" onClick={startGame}>
              Start Game
            </button>
          )}
        </div>
      </div>
    );
    

    Now we can update our styling.

    .wrapper {
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      height: 100vh;
    }
    .canvas {
      display: flex;
      justify-content: center;
      align-items: center;
      background: rgb(151, 216, 148);
      position: relative;
    }
    
    .start {
      font-size: 1rem;
      position: absolute;
      border: 1px solid black;
      background: none;
      border-radius: 1rem;
      padding: 1rem;
      outline: none;
    }
    
    .start:hover {
      border: none;
      background: white;
    }
    
    .game-over {
      position: absolute;
      font-size: 5rem;
      margin-bottom: 10rem;
    }
    

    The fillStyle should be updated as well from red and green to #1C1B17 , so we have this retro feeling/styling of the game.

    We have now a working and styled version of the Classic Snake Game. Well, done. 😎

    What's next?

    • Adding points
    • Game End

    3. 게임 포인트

    Add state for points:

    const [points, setPoints] = useState<number>(0);
    

    Add setPoints to startGame to reset score:

    setPoints(0);
    

    Increase points if apple is eaten, add this to checkAppleCollision :

    setPoints(points + 1);
    

    Add points to game wrapper:

    <p className="points">{points}</p>
    

    Add some styling for the points:

    .points {
      position: absolute;
      bottom: 0;
      right: 1rem;
      font-size: 2rem;
    }
    

    4. 게임 종료

    We have to define a condition, when somebody has finished the game, which is unlikely, though to be feature-complete. The game end, besides a collision, would be the reaching of the maximum of available points. With the current scaling, the calculation is 40x40 = 1600points.

    So we just add a condition to check if the maxPoints are reached and update the state and show some message.

    We add the state to track if the game hasFinished

    const [hasFinishedGame, setHasFinishedGame] = useState<boolean>(
      false,
    );
    

    We add some condition to show the hasFinished message.

    {
      hasFinishedGame && <p className="finished-game">Congratulations</p>;
    }
    
    .finished-game {
      position: absolute;
      top: 60px;
      font-size: 5rem;
      color: red;
      text-decoration: underline;
    }
    

    We add a variable for maxPoints and import it into App.tsx :

    export const maxPoints = 1600;
    

    We add the check if maxPoints have been reached:

    const checkAppleCollision = (newSnake: ICoords[]) => {
      if (newSnake[0].x === apple.x && newSnake[0].y === apple.y) {
        let newApple = createRandomApple();
        while (checkCollision(newApple, newSnake)) {
          newApple = createRandomApple();
        }
        setPoints(points + 1);
        if (points === maxPoints) {
          setHasFinishedGame(true);
          endGame();
        }
        setApple(newApple);
        return true;
      }
      return false;
    };
    

    In case hasFinishedGame has been set to true and we start a new game, the value has to be resetted.

    const startGame = () => {
        setHasFinishedGame(false);
        setPoints(0);
        setIsPlaying(true);
        setSnake(snake_start);
        setApple(apple_start);
        setDirection(direction_start);
        setSpeed(initial_speed);
        setGameOver(false);
        wrapperRef.current?.focus();
      };
    

    That's it. We can always come back and add more features, like sound effects or saving the score in localStorage or ...

    Yay, ✨✨ Congratulations ✨✨. Well, done.

    Your Game should look like this.

    읽어 주셔서 감사합니다. 궁금한 점이 있으면 댓글 기능을 사용하거나 메시지를 보내주세요.

    참조(및 큰 감사): Maksim 및 .

    좋은 웹페이지 즐겨찾기