generated from yandex-praktikum/client-server-template-with-vite
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from Timur233/SOK-27_game-prototype
[SOK-27] game prototype
- Loading branch information
Showing
12 changed files
with
506 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
@import '../../scss/vars'; | ||
|
||
.game-container { | ||
display: flex; | ||
justify-content: center; | ||
flex-direction: column; | ||
align-items: stretch; | ||
background-color: $c_default-background; | ||
font-family: $f_default-font-family; | ||
overflow: hidden; | ||
position: relative; | ||
} | ||
|
||
canvas { | ||
border-radius: $border-radius--default; | ||
box-shadow: 0 4px 8px rgba($c_black, 0.5); | ||
background-image: $img_default-background; | ||
background-size: cover; | ||
} | ||
|
||
.player { | ||
position: absolute; | ||
background-color: $c_button; | ||
border: 2px solid $c_button-top; | ||
border-radius: 4px; | ||
transition: transform 0.1s ease; | ||
} | ||
|
||
.obstacle { | ||
position: absolute; | ||
background-color: $c_button-bottom; | ||
border: 2px solid $c_black; | ||
border-radius: 4px; | ||
} | ||
|
||
.game-text { | ||
color: $c_font-default; | ||
font-size: 16px; | ||
position: absolute; | ||
top: 10px; | ||
left: 10px; | ||
} | ||
|
||
.game-info { | ||
position: absolute; | ||
top: 20px; | ||
left: 20px; | ||
color: $c_font-default; | ||
font-size: 18px; | ||
background: rgba($c_black, 0.5); | ||
padding: 8px 12px; | ||
border-radius: 6px; | ||
} | ||
|
||
.lives { | ||
color: $c_button-top; | ||
font-weight: bold; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import React, { useCallback, useEffect, useRef, useState } from 'react' | ||
import './Game.scss' | ||
import { initializeEnemies } from '@/components/Game/enemy' | ||
import { PLAYER_DEFAULT_PARAMS } from '@/components/Game/player' | ||
import { gameLoop } from '@/components/Game/gameLoop' | ||
import { | ||
handleKeyDown, | ||
handleKeyUp, | ||
updatePlayerMovement, | ||
} from '@/components/Game/controls' | ||
import { ControlsProps, Obstacle } from '@/components/Game/gameTypes' | ||
import { initializeObstacle } from '@/components/Game/obstacle' | ||
import { Modal } from '../common/Modal/Modal' | ||
|
||
const livesUse = 3 | ||
|
||
export const Game: React.FC = () => { | ||
const canvasRef = useRef<HTMLCanvasElement | null>(null) | ||
const playerRef = useRef(PLAYER_DEFAULT_PARAMS) | ||
const enemiesRef = useRef(initializeEnemies(5)) | ||
const obstaclesRef = useRef<Obstacle[]>(initializeObstacle()) | ||
const livesRef = useRef(livesUse) | ||
const [gameStarted, setGameStarted] = useState(false) | ||
const [isPaused, setIsPaused] = useState(false) | ||
const isPausedRef = useRef(false) | ||
const [isGameOver, setIsGameOver] = useState(false) | ||
|
||
const togglePause = useCallback(() => { | ||
setIsPaused(prev => !prev) | ||
isPausedRef.current = !isPausedRef.current // Обновляем ref для паузы | ||
}, []) | ||
|
||
const handleGameOver = useCallback(() => { | ||
setIsGameOver(true) | ||
setIsPaused(true) | ||
isPausedRef.current = true | ||
}, []) | ||
|
||
const loop = useCallback(() => { | ||
if (!isPausedRef.current && !isGameOver && canvasRef.current) { | ||
const context = canvasRef.current.getContext('2d') | ||
if (context) { | ||
const moveProps: ControlsProps = { | ||
playerRef, | ||
obstacles: obstaclesRef.current, | ||
canvasWidth: canvasRef.current.width, | ||
canvasHeight: canvasRef.current.height, | ||
} | ||
updatePlayerMovement(moveProps) | ||
|
||
gameLoop( | ||
context, | ||
playerRef, | ||
enemiesRef, | ||
obstaclesRef.current, | ||
livesRef, | ||
handleGameOver | ||
) | ||
} | ||
requestAnimationFrame(loop) | ||
} | ||
}, [isGameOver, handleGameOver, livesRef]) | ||
|
||
useEffect(() => { | ||
const handleKeyDownWrapper = (event: KeyboardEvent) => | ||
handleKeyDown(event.key) | ||
const handleKeyUpWrapper = (event: KeyboardEvent) => handleKeyUp(event.key) | ||
|
||
window.addEventListener('keydown', handleKeyDownWrapper) | ||
window.addEventListener('keyup', handleKeyUpWrapper) | ||
|
||
return () => { | ||
window.removeEventListener('keydown', handleKeyDownWrapper) | ||
window.removeEventListener('keyup', handleKeyUpWrapper) | ||
} | ||
}, []) | ||
|
||
useEffect(() => { | ||
if (gameStarted && !isPaused) { | ||
requestAnimationFrame(loop) | ||
} | ||
}, [gameStarted, isPaused, loop]) | ||
|
||
const startGame = () => { | ||
setGameStarted(true) | ||
setIsPaused(false) | ||
isPausedRef.current = false | ||
setIsGameOver(false) | ||
livesRef.current = livesUse | ||
playerRef.current = PLAYER_DEFAULT_PARAMS | ||
enemiesRef.current = initializeEnemies(5) | ||
} | ||
|
||
return ( | ||
<div className="game-container"> | ||
<div className="lives">{`Жизни: ${livesRef.current.toString()}`}</div> | ||
<canvas ref={canvasRef} width={800} height={600}></canvas> | ||
|
||
{!gameStarted ? ( | ||
<button onClick={startGame}>Начать игру</button> | ||
) : ( | ||
<button onClick={togglePause}> | ||
{isPaused ? 'Продолжить' : 'Пауза'} | ||
</button> | ||
)} | ||
|
||
<Modal show={isGameOver} onClose={() => setIsGameOver(false)}> | ||
<h2>Игра окончена</h2> | ||
<button onClick={startGame}>Заново</button> | ||
</Modal> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { Enemy, Obstacle, Player } from '@/components/Game/gameTypes' | ||
|
||
export const detectCollision = ( | ||
player: Player, | ||
obstacle: Obstacle | ||
): boolean => { | ||
return ( | ||
player.x < obstacle.x + obstacle.width && | ||
player.x + player.width > obstacle.x && | ||
player.y < obstacle.y + obstacle.height && | ||
player.y + player.height > obstacle.y | ||
) | ||
} | ||
|
||
export const detectEnemyCollision = ( | ||
rect1: Player | Enemy, | ||
rect2: Obstacle | Enemy | ||
): boolean => { | ||
return ( | ||
rect1.x < rect2.x + rect2.width && | ||
rect1.x + rect1.width > rect2.x && | ||
rect1.y < rect2.y + rect2.height && | ||
rect1.y + rect1.height > rect2.y | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { ControlsProps, KeyMap } from '@/components/Game/gameTypes' | ||
|
||
import { detectCollision } from '@/components/Game/collision' | ||
|
||
const keyMap: KeyMap = {} | ||
|
||
// Обработчик нажатия клавиш | ||
export const handleKeyDown = (key: string) => { | ||
keyMap[key] = true | ||
} | ||
|
||
// Обработчик отпускания клавиш | ||
export const handleKeyUp = (key: string) => { | ||
delete keyMap[key] | ||
} | ||
|
||
// Функция для обновления позиции игрока на основе нажатых клавиш | ||
export const updatePlayerMovement = (props: ControlsProps) => { | ||
const speed = props.playerRef.current.speed | ||
let newX = props.playerRef.current.x | ||
let newY = props.playerRef.current.y | ||
|
||
// Определение направления движения | ||
if (keyMap['ArrowUp'] || keyMap['w'] || keyMap['ц']) { | ||
newY -= speed | ||
} | ||
if (keyMap['ArrowDown'] || keyMap['s'] || keyMap['ы']) { | ||
newY += speed | ||
} | ||
if (keyMap['ArrowLeft'] || keyMap['a'] || keyMap['ф']) { | ||
newX -= speed | ||
} | ||
if (keyMap['ArrowRight'] || keyMap['d'] || keyMap['в']) { | ||
newX += speed | ||
} | ||
|
||
// Обработка столкновений с препятствиями | ||
const hasCollision = props.obstacles.some(obstacle => { | ||
return detectCollision( | ||
{ ...props.playerRef.current, x: newX, y: newY }, | ||
obstacle | ||
) | ||
}) | ||
|
||
// Если есть столкновение, то оставить предыдущую позицию | ||
if (!hasCollision) { | ||
// Ограничение движения по краям canvas | ||
newX = Math.max( | ||
0, | ||
Math.min(newX, props.canvasWidth - props.playerRef.current.width) | ||
) | ||
newY = Math.max( | ||
0, | ||
Math.min(newY, props.canvasHeight - props.playerRef.current.height) | ||
) | ||
|
||
// Обновляем позицию игрока в референсе | ||
props.playerRef.current.x = newX | ||
props.playerRef.current.y = newY | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { getRandomEdgePosition } from './utils' | ||
import { Enemy, Player } from '@/components/Game/gameTypes' | ||
|
||
export const initializeEnemies = (numberOfEnemies: number) => { | ||
const initialEnemies: Enemy[] = [] | ||
for (let i = 0; i < numberOfEnemies; i++) { | ||
// количество врагов | ||
const { x, y } = getRandomEdgePosition(800, 600) | ||
const enemy: Enemy = { | ||
x, | ||
y, | ||
width: 30, | ||
height: 30, | ||
speed: 1, | ||
direction: { x: 0, y: 0 }, | ||
} | ||
initialEnemies.push(enemy) | ||
} | ||
return initialEnemies as Enemy[] | ||
} | ||
|
||
export const updateEnemyPositions = ( | ||
player: Player, | ||
enemiesRef: React.MutableRefObject<Enemy[]> | ||
) => { | ||
enemiesRef.current = enemiesRef.current.map(enemy => { | ||
const directionX = player.x - enemy.x | ||
const directionY = player.y - enemy.y | ||
|
||
const magnitude = Math.sqrt(directionX ** 2 + directionY ** 2) | ||
const normalizedX = directionX / magnitude | ||
const normalizedY = directionY / magnitude | ||
|
||
const newX = enemy.x + normalizedX * enemy.speed | ||
const newY = enemy.y + normalizedY * enemy.speed | ||
|
||
return { ...enemy, x: newX, y: newY } | ||
}) | ||
} | ||
|
||
export const respawnEnemies = (enemiesRef: React.MutableRefObject<Enemy[]>) => { | ||
enemiesRef.current = initializeEnemies(5) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { HandlePlayerHit, resetPlayerPosition } from './player' | ||
import { updateEnemyPositions, respawnEnemies } from './enemy' | ||
import { clearCanvas, drawPlayer, drawEnemies, drawObstacles } from './utils' | ||
import { Enemy, Obstacle, Player } from '@/components/Game/gameTypes' | ||
import { detectEnemyCollision } from '@/components/Game/collision' | ||
|
||
/** | ||
* Основной игровой цикл, который обновляет состояние игры и перерисовывает экран каждый кадр. | ||
* @param context - Контекст рисования для Canvas. | ||
* @param playerRef - Ссылка на текущего игрока. | ||
* @param enemiesRef - Ссылка на массив врагов. | ||
* @param obstacles - Массив препятствий. | ||
* @param livesRef - Ссылка на текущее количество жизней игрока. | ||
* @param handleGameOver - Обработчик события окончания игры. | ||
*/ | ||
export const gameLoop = ( | ||
context: CanvasRenderingContext2D, | ||
playerRef: React.MutableRefObject<Player>, | ||
enemiesRef: React.MutableRefObject<Enemy[]>, | ||
obstacles: Obstacle[], | ||
livesRef: React.MutableRefObject<number>, | ||
handleGameOver: () => void | ||
) => { | ||
clearCanvas(context) | ||
|
||
// Обновление позиций врагов | ||
updateEnemyPositions(playerRef.current, enemiesRef) | ||
|
||
// Отрисовка всех игровых объектов | ||
drawObstacles(context, obstacles) | ||
drawPlayer(context, playerRef.current) | ||
drawEnemies(context, enemiesRef.current) | ||
|
||
// Проверка на столкновения между игроком и врагами | ||
enemiesRef.current.forEach(enemy => { | ||
if (detectEnemyCollision(playerRef.current, enemy)) { | ||
// Обработка столкновения: уменьшаем жизни | ||
HandlePlayerHit( | ||
livesRef, | ||
handleGameOver, | ||
() => resetPlayerPosition(playerRef), | ||
() => respawnEnemies(enemiesRef) | ||
) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
export interface Player { | ||
x: number | ||
y: number | ||
width: number | ||
height: number | ||
speed: number | ||
direction: { x: number; y: number } | ||
} | ||
|
||
export interface Enemy { | ||
x: number | ||
y: number | ||
width: number | ||
height: number | ||
speed: number | ||
direction: { x: number; y: number } | ||
} | ||
|
||
export interface Obstacle { | ||
x: number | ||
y: number | ||
width: number | ||
height: number | ||
} | ||
|
||
export interface KeyMap { | ||
[key: string]: boolean | ||
} | ||
|
||
export interface ControlsProps { | ||
playerRef: React.MutableRefObject<Player> | ||
obstacles: Obstacle[] | ||
canvasWidth: number | ||
canvasHeight: number | ||
} |
Oops, something went wrong.