Skip to content

Commit

Permalink
Merge pull request #9 from Timur233/SOK-27_game-prototype
Browse files Browse the repository at this point in the history
[SOK-27] game prototype
  • Loading branch information
shamemask authored Oct 8, 2024
2 parents 7def987 + e2fd186 commit b9dfd18
Show file tree
Hide file tree
Showing 12 changed files with 506 additions and 5 deletions.
58 changes: 58 additions & 0 deletions packages/client/src/components/Game/Game.scss
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;
}
113 changes: 113 additions & 0 deletions packages/client/src/components/Game/Game.tsx
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>
)
}
25 changes: 25 additions & 0 deletions packages/client/src/components/Game/collision.tsx
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
)
}
61 changes: 61 additions & 0 deletions packages/client/src/components/Game/controls.tsx
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
}
}
43 changes: 43 additions & 0 deletions packages/client/src/components/Game/enemy.tsx
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)
}
46 changes: 46 additions & 0 deletions packages/client/src/components/Game/gameLoop.tsx
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)
)
}
})
}
35 changes: 35 additions & 0 deletions packages/client/src/components/Game/gameTypes.tsx
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
}
Loading

0 comments on commit b9dfd18

Please sign in to comment.