Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JavaFX of Tanks

Java CI with Maven

Аркадная игра «Танки» на Java 21 / JavaFX: слоистая архитектура MVVM, доменная логика без единой зависимости от UI-фреймворка, симуляция партии в отдельном потоке и вся редактируемая часть игры (карта, управление, тема, тексты) вынесена в текстовые конфиги.

Управление

Поворот на месте (влево/вправо) плюс газ вперёд/назад вдоль курса — без мгновенного переключения между четырьмя направлениями.

Игрок Вперёд Назад Влево Вправо Выстрел
Игрок 1 W S A D Пробел
Игрок 2 Enter

Общие: P — пауза, Esc — выход в меню.

Раскладка не зашита в код — она читается из controls.properties при каждом запуске.

Второй игрок необязателен и подключается посреди партии, а не только со старта: первое же нажатие клавиши из его раскладки создаёт ему танк и заменяет приглашение в HUD на блок статистики (очки, здоровье, перезарядка). Раунд заканчивается, только когда уничтожены все подключившиеся игроки — отсутствие второго или его более ранняя гибель не влияют на условие поражения для первого. Очки считаются по PlayerId стрелка, а не общим счётчиком на двоих.

Танки толкают друг друга при столкновении на ходу: толкаемый уступает, если ему есть куда сдвинуться (не мешают стена или третий танк), иначе толчок не проходит вовсе. За один тик оба продвигаются на долю попытки хода — GameConfig.PUSH_TRANSFER_FACTOR, а не на всю дистанцию. Готовность орудия — кольцо в HUD, заполняющееся по часовой стрелке при перезарядке; вынесено в неподвижную полосу сверху, а не к танку на поле.

Бонусы и опасности

На карте, помимо стен и точек старта, размечены подбираемые бонусы и взрывоопасные бочки — подбирать бонусы может только танк игрока, ИИ их игнорирует:

Значок на map.txt Что это Эффект
M Медпакет Восстанавливает GameConfig.MEDKIT_HEAL_AMOUNT очков здоровья, не выше максимума
L Доп. жизнь Не расходуется сразу: копится в счётчике игрока и спасает от гибели в раунде — при уничтожении танк вместо выбывания тут же возрождается на стартовой позиции
R Ускоренная перезарядка На GameConfig.RAPID_RELOAD_DURATION_SECONDS секунд перезарядка идёт в GameConfig.RAPID_RELOAD_MULTIPLIER раз быстрее
B Взрывная бочка Не действует сама по себе — но любое попадание пули (хоть игрока, хоть ИИ) детонирует её и наносит ExplosiveBarrel.EXPLOSION_DAMAGE урона всем танкам в радиусе ExplosiveBarrel.EXPLOSION_RADIUS, без разбора команд, включая того, кто стрелял

Возрождение по доп. жизни сопровождается неуязвимостью на GameConfig.RESPAWN_INVULNERABILITY_SECONDS (танк мигает, урон игнорируется) — см. Tank.grantInvulnerability/isInvulnerable.

Детонация бочки рисует расширяющееся кольцо до границы ExplosiveBarrel.EXPLOSION_RADIUSmodel.entity.Explosion, живёт Explosion.DURATION_SECONDS и убирается той же уборкой isDestroyed(), что и пули с бонусами.

Источники текстур

Значки бонусов и бочки — готовые изображения из свободных наборов:

Элемент Файл Источник Автор Лицензия
Медпакет textures/pickup_medkit.png Generic Items — Kenney Kenney (kenney.nl) CC0 1.0
Взрывная бочка textures/barrel.png Topdown Tanks — Kenney Kenney (kenney.nl) CC0 1.0
Доп. жизнь (сердце) textures/pickup_extra_life.png Game icons (heart, diamond, star and lightning bolt) PiXeRaT CC BY-SA 4.0
Ускоренная перезарядка (молния) textures/pickup_rapid_reload.png Game icons (heart, diamond, star and lightning bolt) PiXeRaT CC BY-SA 4.0

Сердце и молния вырезаны из анимированного спрайт-листа автора и обрезаны по границе непрозрачных пикселей.

Запуск

Нужен только JDK 21+ — Maven устанавливать не обязательно: в проекте есть Maven Wrapper (mvnw/mvnw.cmd), который сам скачает нужную версию Maven при первом запуске. Если Maven уже установлен, mvnw можно заменить на mvn.

# Windows (PowerShell/cmd)
.\mvnw.cmd javafx:run

# либо собрать один исполняемый jar со всем нужным внутри
.\mvnw.cmd package
java -jar target\javafx-of-tanks.jar
# Linux/macOS
./mvnw javafx:run
./mvnw package
java -jar target/javafx-of-tanks.jar
./mvnw test

Архитектура

Игра построена по MVVM — границы между слоями проведены так, чтобы model/engine можно было тестировать без единого JavaFX-класса на classpath, а view не знал ни о чём, кроме своей ViewModel и record-снимков.

model     — чистый домен: Tank, Bullet, GameMap… Ни одного import javafx.*
engine    — правила партии: столкновения, ИИ, спавн. Знает про model, не про UI.
viewmodel — граница с UI: JavaFX-свойства, поток симуляции, DTO-снимки для рендера.
view      — Canvas/Scene/Button. Знает только про ViewModel, никогда — про GameWorld.
navigation— composition root: единственное место, которое видит Stage.
config    — парсинг текстовых ресурсов (controls/theme/about) в типизированные объекты.
model/                 PlayerId, AboutInfo — вынесены из entity в корень, иначе capability↔entity дают цикл пакетов
model.capability/       Updatable, Destructible, Damageable, DamageSource, Fireable, BulletSpawnRequest
model.geometry/         Direction, Position, TileCoord
model.entity/           GameObject, Tank, Bullet, Pickup, PickupType, ExplosiveBarrel, Explosion, GameState
model.map/               GameMap, TileType, MapLoadException

engine/                CollisionService, EnemyAiService, GameWorld, GameWorldView, GameConfig
engine.ai/              AiStrategy, AiDecision, RandomAiStrategy
engine.spawn/           SpawnLocationFinder, DefaultSpawnLocationFinder

viewmodel/              GameViewModel, GameSimulationLoop, MenuViewModel, AboutViewModel
viewmodel.dto/           GameSnapshot, TankView, BulletView, PickupView, BarrelView, ExplosionView, PlayerHudInfo

view/                   GameView, GameRenderer, Sprite, TextureSprite, MenuView, AboutView
navigation/              Router, SceneRouter
config/                  ControlsConfig, PlayerControlScheme, SteeringInput, ThemeConfig, AboutContent

GameWorld оркестрирует три отдельных сервиса — CollisionService (геометрия столкновений), EnemyAiService (тайминги решений ИИ), DefaultSpawnLocationFinder (поиск точки возрождения) — каждый тестируется отдельно от остальных.

Функциональные интерфейсы вместо флагов и instanceof

Способности объектов выражены однометодными интерфейсами, а не булевыми полями или цепочками проверок типа:

  • Damageable/DamageSource — кто может получать урон, кто его наносит;
  • FireableTank.tryFire() сам считает точку вылета пули и возвращает Optional<BulletSpawnRequest>, так что этот расчёт не разбросан по местам, откуда стреляют;
  • AiStrategy — тактика вражеского танка — это стратегия, а не набор условий внутри движка; в тестах ей на замену подставляется лямбда, которая никогда не двигается и не стреляет, без единого мока;
  • view.Sprite — та же идея на слое отрисовки: GameRenderer вызывает draw(...), не зная и не проверяя, текстура это (TextureSprite) или код (лямбда — так нарисовано кольцо взрыва, статичным изображением его не изобразить).

Records как DTO между слоями

GameMap, Position, TileCoord, BulletSpawnRequest, AiDecision, AboutInfo и, главное, GameSnapshot/TankView/BulletView — records. GameSnapshot — это единственное, что видит view: он никогда не держит в руках «живой» изменяемый Tank из движка, только его неизменяемый снимок на момент последнего тика.

Второй игрок подключается посреди партии

И Tank, и GameMap, и GameWorld адресуют участников через PlayerId (PLAYER_ONE / PLAYER_TWO), а не единственным полем на танк игрока — это и позволяет второму игроку появиться не с начала раунда, а в любой момент, первым же нажатием своей клавиши:

  • GameMap понимает второй маркер старта на карте — 2;
  • GameWorld.isPlayerAvailable(id) — есть ли для игрока точка старта на карте; isPlayerActive(id) — подключился ли уже; activatePlayer(id) — лениво создаёт танк на стартовой позиции (не-op, если игрок уже активен, карта его не поддерживает или раунд окончен);
  • GameView шлёт запрос на подключение вместе с самой командой на любое нажатие клавиши ещё не подключившегося игрока — оба долетают до GameWorld в одном тике, поэтому танк реагирует на то же нажатие, которым появился;
  • раунд заканчивается, только когда уничтожены все подключившиеся игроки — отсутствие второго или его более ранняя гибель не мешают первому доиграть в одиночку;
  • очки/здоровье/перезарядка считаются по каждому игроку отдельно (Bullet/BulletSpawnRequest несут PlayerId стрелка) и показываются в HUD отдельным блоком — до подключения второго игрока на его месте приглашение.

Конкурентность

Партия считается в фоновом потоке-демоне независимо от кадров экрана:

GameSimulationLoop implements Runnable   — тикает партию на ~60 Гц
        │  ScheduledExecutorService.scheduleAtFixedRate(this, …)
        ▼
   GameWorld.tick(deltaSeconds)          — реальное время, а не «кадры»
        │
        ▼  публикует AtomicReference<GameSnapshot>
   GameView (AnimationTimer, FX-поток)   — читает снимок каждый pulse
  • Шаг симуляции меряется реальным временем (System.nanoTime()), не числом кадров — не завязан на FPS рендера.
  • Рендер синхронизирован с vsync через AnimationTimer.handle(...), который JavaFX сам вызывает раз за такт пайплайна — троттлинг кадров в прикладном коде не нужен.
  • Между потоками — ни одного synchronized. Команды игрока живут в ConcurrentHashMap.newKeySet()/AtomicBoolean (пишет FX-поток, читает поток симуляции), состояние партии — в единственном AtomicReference<GameSnapshot> (record, неизменяем, List.copyOf в компактном конструкторе). Возврат в меню после экрана результатов — единственный переход на FX-поток, через Platform.runLater(...).
  • Поля, которые трогает только сам поток симуляции (таймер результатов, сглаженный FPS), — обычные double/boolean без Atomic*: делить их не с кем.

Конфигурация без пересборки

Всё, что может поменять человек без Java-компилятора под рукой, вынесено в текстовые ресурсы:

Файл Что описывает
map.txt Карта: # стена, P/2 старт игроков, E враг, M/L/R бонусы, B бочка
controls.properties Раскладка клавиш (имена javafx.scene.input.KeyCode)
theme.properties Цветовая палитра экрана (#RRGGBB[AA])
about.properties Текст экрана «О программе»

Тесты

model/engine не содержат импортов JavaFX — юнит-тесты (JUnit 5) не запускают графическое приложение:

  • GameMapTest — разбор карты и её ошибок, бонусы и бочки на клетках;
  • TankTest — перезарядка, урон, плавный поворот, лечение, ускоренная перезарядка, неуязвимость;
  • CollisionServiceTest — стены, толкание танков, дружественный огонь, взрыв бочки (урон по радиусу, начисление убийства), сбор бонусов;
  • GameWorldTest — подключение и гибель второго игрока, очки верному стрелку, возрождение по доп. жизни вместо конца раунда; собирает GameWorld с ИИ-стратегией-заглушкой (лямбдой) вместо рандомизированной.
mvn test

CI/CD

Два workflow в .github/workflows:

Файл Когда запускается Что делает
maven.yml Любой push, любой pull request в main mvnw clean package (JDK 21): юнит-тесты, затем jar; к запуску прикладываются два скачиваемых артефакта — javafx-of-tanks (сам jar) и jacoco-report (отчёт покрытия)
release.yml push тега вида v* (например, v2.0.0) mvnw clean package и публикация javafx-of-tanks.jar в GitHub Releases с автоматически сгенерированными release notes

Артефакты maven.yml живут ограниченное время и качаются только залогиненным в GitHub; постоянная публичная ссылка на скачивание появляется только через тег и release.yml.

Автор

Непрокин Иван

About

Аркадная игра "Танки" на JavaFX с архитектурой MVVM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages