Разработка игры на WPF
Вчера на работе прошел второй хакатон. На этот раз темой был рабочий проект — все, что давно хотелось сделать, но в спринт никогда не попадет. Мы с Сашей Решетниковым решили делать игру, которую можно будет встроить в клиентское приложение в виде пасхального яйца.
Идея игры
Решили делать ремейк старой игры «Попади в меня кирпич».
Геймплей прост — игрок должен уворачиваться от кирпичей. Двигаться можно влево и вправо. Кирпичи отталкиваются друг от друга, от стен и от игрока.
Переключение экранов
Для простоты встраивания вся игра состоит из одного UserControl. Размер контрола захардкодили, так как это размера очень сильно зависит сложность игры.
Управление экранами игры очень простое — видимому экрану игры ставится Visibility = Visible, остальным — Collapsed.
Непосредственно игровой экран содержит Canvas, на котором и рисуются игровые объекты.
Обработка действий пользователя
При старте игры запускается таймер, который обрабатывает состояние игровых объектов и перерисовывает игру.
Обработчики нажатия кнопок навешиваются на окно, а не на контрол. Это нужно для избежания проблем с фокусом.
При старте игры обработчики навешиваются, при проигрыше — обработчики снимаются.
/// <summary> /// Начать игру. /// </summary> private void StartGame() { // Показываем игровой экран. this.GameMenu.Visibility = System.Windows.Visibility.Collapsed; this.GameGrid.Visibility = System.Windows.Visibility.Visible; // Навешиваем обработчики кнопок. Application.Current.MainWindow.KeyDown += this.GameCanvasKeyDown; Application.Current.MainWindow.KeyUp += this.GameCanvasKeyUp; // Инициализируем стартовые значения. this.scores = 0; this.lives = 5; this.InitPlayer(); this.InitBricks(); // Запускаем таймер. this.timer.Start(); } /// <summary> /// Завершить игру. /// </summary> private void EndGame() { // Останавливаем таймер. this.timer.Stop(); // Очищаем данные. this.GameCanvas.Children.Clear(); this.playerDirection = Direction.None; // Показываем экран с результатами. this.GameGrid.Visibility = System.Windows.Visibility.Collapsed; this.GameScoreBoard.Visibility = System.Windows.Visibility.Visible; // Снимаем обработчики. Application.Current.MainWindow.KeyDown -= this.GameCanvasKeyDown; Application.Current.MainWindow.KeyUp -= this.GameCanvasKeyUp; } /// <summary> /// Обновить направление движения игрока. /// </summary> private void UpdateDirection() { if (Keyboard.IsKeyDown(Key.Left)) this.playerDirection = Direction.Left; else if (Keyboard.IsKeyDown(Key.Right)) this.playerDirection = Direction.Right; else this.playerDirection = Direction.None; } /// <summary> /// Обновить координаты игрока. /// </summary> private void UpdateCoordinates() { // Обработка направления движения. switch (playerDirection) { case Direction.Left: playerCoordinates.X -= GameSettings.PlayerVelocityPerTick; break; case Direction.Right: playerCoordinates.X += GameSettings.PlayerVelocityPerTick; break; } // Обработка столкновений со стенами. if (playerCoordinates.X < GameSettings.PlayerRadius) playerCoordinates.X = GameSettings.PlayerRadius; if (playerCoordinates.X > GameSettings.GameWidth - GameSettings.PlayerRadius) playerCoordinates.X = GameSettings.GameWidth - GameSettings.PlayerRadius; }
Коллизии и отталкивания
У каждого кирпича хранятся координаты левого верхнего угла. Кирпичи столкнулись, если расстояние по вертикали между ними меньше высоты кирпича и расстояние по горизонтали меньше ширины кирпича.
Столкновения тоже сделали по простому — при столкновении кирпичи обмениваются векторами скоростей. Это конечно некорректно с точки зрения физики, но на сложные расчеты скоростей нам не хватило времени. Да и в оригинальной игре вроде так же сделано.
if (Math.Abs(brick.X - otherbrick.X) < sizeX && Math.Abs(brick.Y - otherbrick.Y) < sizeY) { var otherdx = otherbrick.dX; var otherdy = otherbrick.dY; otherbrick.dX = brick.dX; otherbrick.dY = brick.dY; brick.dX = otherdx; brick.dY = otherdy; }
Результат
В итоге у нас получилась вот такая вот игра: