Java — Java Survivors
Разработчику Средний уровень
Материалы трека приводятся к единому формату: полные листинги для копирования на каждом этапе, блок "Разбор" и раздел "Полная ревизия" в конце статьи.
- Гарантированно запускаемые эталоны для сверки сейчас: Python — Battle City (GitHub), Python — Match3 (GitHub,
match3.py), Python — Ping Pong (#full-revision). - Раздел полной ревизии в этой статье ещё в работе — идите по этапам по порядку; если код перестал запускаться, сравните проект с этими эталонами.
Как проходить практикум
- Копируйте целиком файлы из блоков кода каждого этапа.
- После каждого этапа запускайте проект (команда указана в главе) и пройдите чек-лист самопроверки.
- Если застряли — методология в разделе Практикум разработки игр — о разделе; для сверки готовые треки Battle City, Match3 и Ping Pong.
О практикуме
Vampire Survivors и его клоны (survivor-like, horde survival) — арена сверху, волны врагов, автоатаки без прицеливания, опыт с поля и пауза на выбор улучшений. Полная реализация с десятками оружий, персонажами и сохранениями — в репозитории Java Survivors (локально: F:\Projects\JVM\Java\Java Survivors). Стек там — Java 17, Swing, Java2D, без сторонних игровых движков.
В этом практикуме соберём узнаваемый прототип с нуля — от пустого окна до врагов, магического болта, опыта, экрана прокачки, нескольких оружий, HUD и меню. Графика — круги и прямоугольники (как в ранней версии до спрайтов); в конце — карта расширений до уровня полного проекта.
Нужны базовый Java (классы, enum, коллекции, List, Iterator) и понимание ООП. Каждый этап — запускаемый код: после шага проект компилируется и показывает новую механику. Сборка — Maven; IDE — IntelliJ IDEA, VS Code с Extension Pack for Java или Cursor.
Управление в финальной версии практикума
| Клавиша | Действие |
|---|---|
W A S D или стрелки |
Движение |
Пробел |
Рывок (dash) |
1 2 3 |
Выбор улучшения на экране level-up |
Enter |
Старт с меню |
R |
Перезапуск после поражения |
Esc |
Выход |
Маршрут чтения
- Архитектура — как устроен проект до первой строки кода.
- Зависимости и структура — JDK, Maven, пакеты.
- Этап 0 — минимальный запуск — окно и игровой поток.
- Этапы 1–18 — по одной (или паре) механик за шаг.
- Итоговая самопроверка — чек-лист и связь с полным репозиторием.
Что должно получиться к этапу 18
| Механика | Описание |
|---|---|
| Цикл | Отдельный поток + dt, отрисовка в paintComponent |
| Игрок | Круг, HP, реген, броня, движение по WASD |
| Враги | Спавн с краёв экрана, преследование, рост сложности |
| Оружие | Авто "магический болт", тройной залп, кольцо импульса |
| Опыт | Сферы XP, магнит, уровни, пауза на 3 улучшения |
| Урон | Снаряды, контакт, рывок с неуязвимостью |
| HUD | HP, полоска XP, время, счёт, волна |
| Состояния | Меню, игра, game over |
Полная игра с персонажами, магазином и сохранениями — в Java Survivors. Практикум ниже объясняет, как такой проект собирается с нуля; после этапа 18 можно построчно сравнить свой com.survivors с com.diabloid в репозитории.
Сводка этапов
| Этап | Тема | Новое в запуске |
|---|---|---|
| 0 | Цикл | Окно, поток, dt, repaint |
| 1 | FSM | GameState, меню, Esc |
| 2 | Игрок | WASD, круг на арене |
| 3 | Враги | Спавн с краёв, преследование |
| 4 | Снаряды | Авто "магический болт" |
| 5 | Урон | Попадания, счёт, смерть врага |
| 6 | XP | Орбы, магнит, уровень |
| 7 | Перки | Пауза, 3 выбора, 1 2 3 |
| 8 | HUD | HP, XP, время, волна |
| 9 | Оружия | WeaponType, залп, кольцо |
| 10 | Выживание | Контакт, броня, реген |
| 11 | Рывок | Dash, i-frames |
| 12 | Juice | Цифры урона, частицы |
| 13 | Мета-ран | Game over, resetRun |
| 14 | Архетипы | SPEEDER, TANK, SHOOTER |
| 15 | Босс | Сложность по времени |
| 16 | Модули | WeaponManager, файлы |
| 17 | Арт | PNG, MediaTracker |
| 18 | Референс | Таблица фич полной игры |
Чем survivor-like отличается от "обычного" шутера
| Аспект | Классический twin-stick | Survivor-like (VS, Java Survivors) |
|---|---|---|
| Прицеливание | Игрок целится | Оружие само выбирает цель / паттерн |
| Прогресс в ране | Подбор на карте | Level-up с выбором из 3 карт |
| Давление | Волны по скрипту | Непрерывный спавн + рост difficulty по worldTime |
| Поражение | Жизни / чекпоинты | Один HP-бар, короткий рестарт |
| Сессия | 5–15 мин уровень | 10–30 мин один ран до смерти |
Архитектура
Прежде чем писать код, зафиксируем что из чего состоит и как данные текут по кадру. Референсная архитектура совпадает с Java Survivors: центральная панель владеет списками сущностей и вызывает обновление/отрисовку.
Жанр и петля геймплея
Survivor-like строится на положительной обратной связи:
- Враги приходят волнами → игрок убивает автооружием.
- Выпадает опыт → уровень → выбор перка (сильнее).
- Сложность растёт по времени выживания → нужны новые перки.
- Смерть → короткий цикл "ещё раз".
flowchart LR
A[Враги] --> B[Автоатаки]
B --> C[XP / лут]
C --> D[Level-up]
D --> E[Усиление]
E --> B
A --> F[Контактный урон]
F --> G[HP игрока]
G --> H{жив?}
H -->|нет| I[Game Over]
H -->|да| A
Игровой цикл (Swing)
В desktop-играх на Swing нельзя долго блокировать EDT (Event Dispatch Thread). В Java Survivors логика крутится в отдельном потоке, а перерисовка — через repaint() → paintComponent:
flowchart TD
A[main → SwingUtilities.invokeLater] --> B[JFrame + GamePanel]
B --> C[Запуск потока game-loop]
C --> D{running?}
D -->|да| E[dt из System.nanoTime]
E --> F[update dt]
F --> G[repaint]
G --> H[Thread.sleep ~8ms]
H --> D
D -->|нет| I[закрытие]
G --> J[paintComponent на EDT]
J --> K[draw мир + HUD]
На каждом шаге update (если не меню и не пауза прокачки):
- Увеличить
worldTime, таймеры спавна и волн. - Движение игрока, реген HP.
- Спавн врагов, ИИ преследования.
- Кулдауны оружия → новые снаряды.
- Движение снарядов, столкновения, смерть врагов → XP.
- Магнит XP, проверка level-up.
- Контактный урон, проверка game over.
Слои приложения
| Слой | Ответственность | Примеры в полном проекте |
|---|---|---|
| Ввод | Клавиши, мышь на экране улучшений | KeyHandler, MouseHandler |
| Мир | Размер арены, время, волны | WIDTH, HEIGHT, worldTime, wave |
| Акторы | Игрок, враги, снаряды, XP | Player, Enemy, Projectile, XpOrb |
| Правила | Урон, опыт, перки, оружие | damageEnemy, addXp, rollUpgradeChoices |
| Представление | Java2D, HUD, оверлеи | paintComponent, Graphics2D |
| Мета | Сохранения, персонажи, магазин | SaveSystem, CharacterDef (этап 18+) |
Слой правил не рисует напрямую — он меняет поля объектов; paintComponent только читает состояние.
Координаты и коллизии
Арена — пиксели, начало (0, 0) в левом верхнем углу. Игрок и враги — круги (x, y, radius). Столкновение двух кругов:
static boolean circlesHit(double x1, double y1, double r1,
double x2, double y2, double r2) {
double dx = x1 - x2;
double dy = y1 - y2;
double sum = r1 + r2;
return dx * dx + dy * dy <= sum * sum;
}
Для производительности в горячих циклах используют distanceSq без Math.sqrt.
Шаг времени dt и cap
dt — секунды с прошлого кадра. Без ограничения свёрнутое окно даёт скачок dt на секунды, и игрок "телепортируется" сквозь врагов:
dt = Math.min(dt, 0.033); // не больше ~2 кадров при 60 FPS
Все перемещения и кулдауны записываются в форме x += speed * dt, cooldown -= dt — так игра ведёт себя одинаково на 60 и 120 Гц монитора.
Порядок update на зрелом этапе
Фиксированный порядок снижает баги "снаряд попал до спавна врага":
sequenceDiagram
participant Loop as game-loop
participant U as update
participant W as weapons
participant S as spawner
participant E as enemies
participant P as projectiles
participant X as xp
Loop->>U: dt
U->>U: worldTime, wave, dash
U->>U: player.move + heal
U->>W: updateWeapons
U->>S: spawnEnemies
U->>E: updateEnemies
U->>P: updateProjectiles
U->>X: updateXpOrbs + level-up?
U->>U: contact damage, game over
Карта репозитория Java Survivors
| Файл | Роль |
|---|---|
JavaSurvivors.java |
Точка входа → DiabloidGame |
DiabloidGame.java |
JFrame, вложенный GamePanel, цикл, отрисовка, 90% логики рана |
Player.java |
Статы, кулдауны всех оружий, reset() |
Enemy.java / EnemyKind.java |
Враг и архетип |
Projectile.java |
Снаряд игрока/врага, pierce, статусы |
WeaponType.java |
Все виды оружия (болт, молния, стихии…) |
WeaponManager.java |
Делегат updateWeapons |
EnemySpawner.java |
Делегат spawnEnemies |
SaveSystem.java / SaveData.java |
Мета-прогресс между ранами |
ParticleSystem.java |
Частицы и следы |
Учебный проект намеренно не копирует 1700 строк сразу — вы повторяете те же списки + enum + пауза на upgrade, расширяя по этапам.
Конечный автомат (упрощённый)
stateDiagram-v2
[*] --> MENU
MENU --> PLAYING : Enter
PLAYING --> PAUSED_UPGRADE : level-up
PAUSED_UPGRADE --> PLAYING : выбор 1/2/3
PLAYING --> GAME_OVER : HP <= 0
GAME_OVER --> PLAYING : R
MENU --> [*] : Esc
PLAYING --> [*] : Esc
GAME_OVER --> [*] : Esc
В полном Java Survivors добавлены UpgradeState.PAUSED_FOR_SHOP, выбор персонажа и сохранение — см. этап 18.
Списки сущностей на кадре
classDiagram
class GamePanel {
+Player player
+List~Enemy~ enemies
+List~Projectile~ projectiles
+List~XpOrb~ xpOrbs
+Set~WeaponType~ unlockedWeapons
+update(dt)
+paintComponent(g)
}
class Player {
+double x y
+double hp maxHp
+int level xp
}
class Enemy {
+double x y hp
+double speed
}
class Projectile {
+double x y vx vy
+double damage radius
}
GamePanel --> Player
GamePanel --> Enemy
GamePanel --> Projectile
GamePanel --> XpOrb
Целевая структура файлов
К этапу 6 достаточно одного SurvivorsGame.java с вложенным GamePanel. Дальше выносим классы — как в репозитории com.diabloid:
java-survivors-lab/
├── pom.xml
└── src/main/java/com/survivors/
├── JavaSurvivors.java # main
├── SurvivorsGame.java # JFrame + GamePanel (этапы 0–10)
├── GameState.java
├── UpgradeState.java
├── WeaponType.java
├── Player.java # этап 14+
├── Enemy.java
├── EnemyKind.java
├── Projectile.java
├── XpOrb.java
└── DamageNumber.java
Пакет в учебнике — com.survivors; в полном проекте — com.diabloid (историческое имя "Diabloid").
Референсный репозиторий использует встроенный Java2D — нулевые внешние зависимости, один JAR после mvn package. LibGDX уместен для кроссплатформы и GPU-спрайтов; для понимания survivor-like логики Swing достаточен. Базовые JFrame, кнопки и EDT — Lab — Java Swing; теория GUI — JavaFX и GUI.
Зависимости и подготовка окружения
Требования
- JDK 17+ (как в
pom.xmlрепозитория). - Maven 3.8+ (или встроенный Maven в IDE).
- Опционально — Git для клонирования референса.
Создание проекта
mkdir java-survivors-lab && cd java-survivors-lab
pom.xml (совпадает по духу с Java Survivors):
Папки:
src/main/java/com/survivors/
Сборка и запуск
mvn -q compile exec:java -Dexec.mainClass="com.survivors.JavaSurvivors"
Или после упаковки:
mvn -q package
java -jar target/java-survivors-lab-1.0-SNAPSHOT.jar
IntelliJ IDEA / Cursor
- File → Open — папка с
pom.xml. - Дождитесь индексации Maven (импорт JDK 17).
- ПКМ по
JavaSurvivors.java→ Run 'JavaSurvivors.main()'. - Working directory — корень проекта (для
assets/на этапе 17).
Если поставить весь while (running) в main без отдельного потока, Swing "замрёт". В практикуме логика — в Thread с именем game-loop, отрисовка — только через repaint() на EDT.
Палитра (единые цвета)
Вынесите цвета в GameColors.java, чтобы HUD и сущности не расходились:
Структура assets/ (этап 17)
java-survivors-lab/
├── assets/
│ ├── player.png
│ ├── enemy_normal.png
│ ├── enemy_tank.png
│ └── background.jpg
└── src/main/java/...
Запускайте JAR из корня, где лежит assets/, иначе Toolkit.getImage("assets/...") вернёт пустую картинку.
Сохраняйте .java в UTF-8. Русские строки в HUD и улучшениях иначе превратятся в "кракозябры" при сборке на Windows с неверной кодировкой по умолчанию.
Этап 0 — минимальный запускаемый код
Цель — JFrame, тёмная панель, поток игрового цикла, стабильный dt, выход по закрытию окна.
src/main/java/com/survivors/JavaSurvivors.java:
package com.survivors;
import javax.swing.SwingUtilities;
public final class JavaSurvivors {
public static void main(String[] args) {
SwingUtilities.invokeLater(SurvivorsGame::new);
}
}
src/main/java/com/survivors/SurvivorsGame.java:
Самопроверка этапа 0
-
mvn compileбез ошибок. - Окно 960×540, тёмный фон, подпись на экране.
- Нет зависаний при перетаскивании окна (логика не в EDT).
Временно рисуйте 1.div(dt) в углу экрана — если значение скачет ниже 30 при пустой сцене, ищите тяжёлую работу в paintComponent (там должна быть только отрисовка).
Этап 1 — состояния игры и ввод
Цель — enum GameState, меню с Enter, выход Esc, заготовка PLAYING.
Добавьте GameState.java:
package com.survivors;
public enum GameState {
MENU, PLAYING, GAME_OVER
}
В GamePanel — поля и полный обработчик клавиш (на этапе 2 понадобятся keyReleased):
В paintComponent — текст меню:
if (gameState == GameState.MENU) {
g.drawString("JAVA SURVIVORS — Enter: старт", 280, HEIGHT / 2);
}
Самопроверка
- На старте видно меню;
Enterпереключает на пустую арену. -
Escзакрывает игру.
Этап 2 — игрок и движение
Цель — класс Player, WASD/стрелки, ограничение внутри экрана.
Player.java:
В GamePanel — флаги клавиш и отрисовка круга:
Самопроверка
- Синий круг двигается плавно, не выходит за края.
Этап 3 — враги и спавн с краёв
Цель — Enemy, список enemies, спавн по таймеру, преследование игрока.
Enemy.java (минимум):
В GamePanel:
Отрисовка врагов — красные круги.
Самопроверка
- Каждые ~0.5–1 с появляется новый враг с края.
- Враги сходятся к игроку.
Этап 4 — снаряды и магический болт
Цель — Projectile, автоатака по ближайшему врагу с кулдауном.
Projectile.java:
В Player добавьте double shotCooldown = 0.2;.
В GamePanel:
updateProjectiles и отрисовка (этап 4–5):
В Java Survivors большинство снарядов летит к findNearestEnemy() — простая эвристика, которая ощущается как прицеливание без мыши. Паттерны вроде "кольцо импульса" стреляют во все стороны и не используют цель.
Самопроверка
- Снаряды летят к ближайшему врагу без клика мыши.
Этап 5 — урон, смерть врага, очки
Цель — столкновение снаряд–враг, единая точка урона damageEnemy, счёт.
Самопроверка
- Враги исчезают от попаданий, счёт растёт.
Этап 6 — опыт, магнит, уровень
Цель — XpOrb, выпадение при смерти, притягивание, level / xpToNext.
XpOrb.java:
package com.survivors;
public final class XpOrb {
public double x, y;
public final int value;
public XpOrb(double x, double y, int value) {
this.x = x; this.y = y; this.value = value;
}
}
В Player:
public int level = 1;
public int xp = 0;
public int xpToNext = 10;
public double magnetRadius = 90;
При убийстве врага: xpOrbs.add(new XpOrb(e.x, e.y, e.xpValue));
Поле int pendingLevelUps — используем на следующем этапе.
Самопроверка
- Зелёные/бирюзовые точки XP тянутся к игроку и исчезают при подборе.
- В консоли или HUD позже видно рост
level.
Этап 7 — пауза на выбор улучшений
Цель — UpgradeState, три случайных перка, клавиши 1 2 3.
UpgradeState.java:
package com.survivors;
public enum UpgradeState {
NONE, PAUSED_FOR_UPGRADE
}
В Player добавьте damageMultiplier, attackSpeedMultiplier, flatDamageBonus (по умолчанию 1.0 и 0).
В update в начале:
if (upgradeState == UpgradeState.PAUSED_FOR_UPGRADE) {
return;
}
В keyPressed:
if (upgradeState == UpgradeState.PAUSED_FOR_UPGRADE) {
if (e.getKeyCode() == KeyEvent.VK_1 && upgradeChoices.size() > 0) pickUpgrade(0);
if (e.getKeyCode() == KeyEvent.VK_2 && upgradeChoices.size() > 1) pickUpgrade(1);
if (e.getKeyCode() == KeyEvent.VK_3 && upgradeChoices.size() > 2) pickUpgrade(2);
}
pickUpgrade и очередь нескольких уровней за раз:
Оверлей level-up (рисуется поверх арены, логика на паузе):
private void drawUpgradeOverlay(Graphics2D g2) {
g2.setColor(new Color(0, 0, 0, 170));
g2.fillRect(0, 0, WIDTH, HEIGHT);
g2.setFont(g2.getFont().deriveFont(Font.BOLD, 28f));
g2.setColor(Color.WHITE);
g2.drawString("LEVEL UP — выберите улучшение", 260, 120);
g2.setFont(g2.getFont().deriveFont(Font.PLAIN, 20f));
for (int i = 0; i < upgradeChoices.size(); i++) {
g2.drawString((i + 1) + " — " + upgradeChoices.get(i), 280, 200 + i * 48);
}
}
В applyUpgrade добавьте ветки для оружия (этап 9):
case "Оружие: Тройной залп" -> unlockedWeapons.add(WeaponType.TRIPLE_CAST);
case "Оружие: Кольцо импульса" -> unlockedWeapons.add(WeaponType.PULSE_RING);
Самопроверка
- При level-up игра замирает, видны 3 варианта.
- После
2бой продолжается, статы изменились.
Этап 8 — HUD (HP, XP, время, счёт)
Цель — полоски HP/XP, таймер worldTime, волна.
В shootMagicBolt учитывайте множители:
double damage = 18 * player.damageMultiplier + player.flatDamageBonus;
player.shotCooldown = Math.max(0.08, 0.35 / player.attackSpeedMultiplier);
Порядок отрисовки в paintComponent (снизу вверх):
Самопроверка
- HUD читается поверх арены, не перекрывает игрока в центре.
Этап 9 — несколько оружий (enum)
Цель — WeaponType, Set<WeaponType> unlockedWeapons, тройной залп и кольцо импульса.
WeaponType.java:
package com.survivors;
public enum WeaponType {
MAGIC_BOLT, TRIPLE_CAST, PULSE_RING
}
В Player добавьте отдельные кулдауны:
public double tripleCooldown = 0.8;
public double pulseCooldown = 1.2;
public double damageMultiplier = 1.0;
public double attackSpeedMultiplier = 1.0;
public double flatDamageBonus = 0.0;
Полные реализации (адаптация из Java Survivors):
Расширьте Projectile полем public int pierce = 0; — на бонусном этапе снаряд с pierce > 0 не удаляется после первого попадания.
В пул rollUpgradeChoices добавьте строки оружия (только если ещё не разблокировано):
if (!unlockedWeapons.contains(WeaponType.TRIPLE_CAST)) {
pool.add("Оружие: Тройной залп");
}
if (!unlockedWeapons.contains(WeaponType.PULSE_RING)) {
pool.add("Оружие: Кольцо импульса");
}
Самопроверка
- После выбора "Тройной залп" видны дополнительные снаряды с отдельным ритмом.
Этап 10 — контактный урон, броня, реген
Цель — урон при наложении кругов, armorReduction, пассивный хил.
В Player:
public double regen = 1.5;
public double armorReduction = 0.1;
public void heal(double dt) {
hp = Math.min(maxHp, hp + regen * dt);
}
Самопроверка
- При "объятии" толпой HP падает ступенями, не каждый кадр.
Этап 11 — рывок (dash)
Цель — Пробел, краткая неуязвимость, кулдаун (как DASH_DURATION в референсе).
Визуально — белая обводка Ellipse2D вокруг игрока на время рывка. В Java Survivors рывок также сбрасывает серию без урона (noDamageTime) — можно добавить для достижений в мета-сохранении.
Самопроверка
- Рывок проскальзывает сквозь орду без урона на ~0.15 с.
Этап 12 — всплывающий урон и частицы
Цель — DamageNumber, простые Particle при убийстве.
DamageNumber.java:
При damageEnemy добавляйте damageNumbers.add(new DamageNumber(e.x, e.y - 10, String.format("%.0f", dmg)));.
Обновление — y -= 40 * dt, life -= dt, удаление при life <= 0. Отрисовка — g2.drawString.
Particle.java и простой спавн при убийстве:
package com.survivors;
public final class Particle {
public double x, y, vx, vy;
public double life = 0.5;
public final java.awt.Color color;
public Particle(double x, double y, double vx, double vy, java.awt.Color color) {
this.x = x; this.y = y; this.vx = vx; this.vy = vy;
this.color = color;
}
}
В damageEnemy при смерти вызывайте spawnKillParticles(enemy.x, enemy.y).
Самопроверка
- При попадании видны жёлтые цифры урона.
Этап 13 — меню, game over, перезапуск
Цель — сброс рана по R, экран поражения со статистикой.
Player.reset() — сброс статов рана (упрощённо):
public void reset() {
x = 480; y = 270;
maxHp = 100; hp = 100;
moveSpeed = 220;
regen = 1.5;
armorReduction = 0.1;
magnetRadius = 90;
damageMultiplier = 1.0;
attackSpeedMultiplier = 1.0;
flatDamageBonus = 0.0;
level = 1; xp = 0; xpToNext = 10;
shotCooldown = 0; tripleCooldown = 0; pulseCooldown = 0;
}
Скелет update к этапу 13
paintComponent для GAME_OVER — "Поражение", счёт, время, "R — заново".
Самопроверка
- После смерти
Rзапускает чистый ран.
Этап 14 — типы врагов (enum)
Цель — EnemyKind — NORMAL, SPEEDER, TANK (упрощённо).
EnemyKind.java:
package com.survivors;
public enum EnemyKind {
NORMAL, SPEEDER, TANK, SHOOTER, BOSS
}
Поле public EnemyKind kind = EnemyKind.NORMAL; в Enemy. Пример spawnEnemy с вероятностями (как в референсе, упрощённо):
| Тип | Визуал | Поведение |
|---|---|---|
| NORMAL | красный | идёт к игроку |
| SPEEDER | оранжевый, меньше | быстрее |
| TANK | тёмно-красный, больше | медленный, много HP |
| SHOOTER | розоватый | держит дистанцию ~180px, стреляет |
ИИ стрелка (в updateEnemies):
Добавьте в Enemy поле double attackCooldown = 0; и в Projectile флаг boolean fromEnemy = false. В updateProjectiles обрабатывайте вражеские снаряды отдельно — урон игроку, как в Java Survivors.
Самопроверка
- На поле смешаны разные силуэты, темп игры разнообразнее.
Этап 15 — усложнение спавна и босс (мини)
Цель — формула difficulty от worldTime, пачки врагов, редкий босс.
Спавн-таймер из референса (ускоряется со временем):
Сигнатуру spawnEnemy измените на spawnEnemy(double difficulty) и используйте множитель в HP/скорости.
Босс — большой HP, медленный, много XP. Фаза 1 в полной игре — кольцо из 18 вражеских снарядов при attackCooldown <= 0; фаза 2 — дополнительный спавн мелочи. Реализация — в updateEnemies для EnemyKind.BOSS в DiabloidGame.java.
Самопроверка
- Раз в ~2 минуты появляется крупный враг сверху.
Этап 16 — разнесение по файлам и делегаты
Цель — повторить структуру Java Survivors: вынести Player, Enemy, Projectile, оставить в GamePanel только оркестрацию.
Создайте тонкие обёртки (как в репозитории):
final class EnemySpawner {
private final SurvivorsGame.GamePanel game;
void tick(double dt) { game.spawnEnemies(dt); }
}
final class WeaponManager {
private final SurvivorsGame.GamePanel game;
void tick(double dt) { game.updateWeapons(dt); }
}
В update(dt):
weaponManager.tick(dt);
enemySpawner.tick(dt);
Это тот же приём, что WeaponManager / EnemySpawner в com.diabloid — делегирование без преждевременного раздувания одного файла на 1700+ строк.
Самопроверка
- Поведение идентично этапу 15,
DiabloidGame.java/SurvivorsGame.javaкороче и читаемее.
Этап 17 — спрайты и фон
Цель — загрузка PNG из assets/, MediaTracker, fallback на круги.
Загрузка с ожиданием декодирования (как в DiabloidGame.loadAssets):
При ошибке загрузки — imagesLoaded = false, рисуем примитивы. Скопируйте ассеты из клонированного Java Survivors или нарисуйте 32×32 в любом редакторе.
Самопроверка
- С ассетами — спрайты; без папки
assets— игра не падает.
Этап 18 — карта пути к полному Java Survivors
Цель — понять, что уже есть в репозитории и что добавить самостоятельно.
| Функция в Java Survivors | Класс / зона | В практикуме |
|---|---|---|
| 10+ персонажей с разным стартовым оружием | CharacterDef, выбор в меню |
этап 18+ |
Десятки WeaponType, стихии |
WeaponType, fireExtraWeapon |
частично (этап 9) |
| Цепная молния, пила | LightningEffect, SawBladeEffect |
самостоятельно |
| Статусы burn/slow/poison | StatusEffect, StatusEffectType |
самостоятельно |
| Магазин между волнами | PAUSED_FOR_SHOP, rollShopChoices |
самостоятельно |
| Сохранение мета-прогресса | SaveSystem, SaveData |
самостоятельно |
| Монеты на карте | CoinPickup, runCoins |
самостоятельно |
Клонирование референса:
git clone https://github.com/Spirzen/Java-Survivors.git
cd Java-Survivors
mvn -q package
java -jar target/java-survivors-1.0-SNAPSHOT.jar
Сравните свой com.survivors с com.diabloid — совпадают имена паттернов (списки, Iterator, enum оружия, пауза на upgrade).
Ветки перков в полной игре
В референсе улучшения разбиты по PerkBranch — ATTACK, DEFENSE, SUPPORT; на level-up предлагается по одному варианту с каждой ветки (до трёх строк на экране). Метод rollUpgradeChoices() в DiabloidGame — образец для расширения учебного пула.
Магазин каждые 3 волны
UpgradeState.PAUSED_FOR_SHOP открывается при wave % 3 == 0 — трата runCoins на постоянные бонусы (permanentShopUpgrades). Это мета-слой внутри рана, между "чистым" survivor-like и roguelike.
Бонус — пронзание, аура, цепная молния
После этапа 18 можно добавить три узнаваемых механики из Java Survivors без переписывания архитектуры.
Пронзающие снаряды (pierce)
В updateProjectiles после damageEnemy:
if (hit != null) {
damageEnemy(hit, p.damage);
if (p.pierce > 0) {
p.pierce--;
} else {
it.remove();
}
}
shootPierceLance — один луч с pierce = 2 и повышенным уроном.
Аура урона (DAMAGE_AURA)
Тик раз в ~0.28 с, урон всем врагам в радиусе auraRadius без снарядов:
private void updateDamageAura(double dt) {
player.auraTickCooldown -= dt;
if (player.auraTickCooldown > 0) return;
player.auraTickCooldown = 0.28;
double r = player.auraRadius;
double dmg = 7.5 * totalDamageMultiplier();
for (Enemy e : new ArrayList<>(enemies)) {
if (distSq(player.x, player.y, e.x, e.y) <= r * r) {
damageEnemy(e, dmg);
}
}
}
Цепная молния
Мгновенный урон по цепочке до трёх врагов + класс LightningEffect только для отрисовки линий между ними на 0.2 с. Логика выбора second/third в радиусе 210px — см. castChainLightning() в репозитории.
Итоговая самопроверка проекта
| # | Критерий | Да / нет |
|---|---|---|
| 1 | Maven-сборка, main в манифесте shade |
|
| 2 | Игровой цикл в отдельном потоке, capped dt |
|
| 3 | Игрок двигается, не выходит за экран | |
| 4 | Враги спавнятся с краёв и преследуют | |
| 5 | Автоатака без клика по врагам | |
| 6 | XP, уровень, экран выбора 3 перков | |
| 7 | Минимум 2 дополнительных оружия через перки | |
| 8 | HUD — HP, XP, время, счёт | |
| 9 | Контактный урон, рывок, game over + restart | |
| 10 | Код разнесён по нескольким .java |
Типичные ошибки
| Симптом | Вероятная причина | Что сделать |
|---|---|---|
| Чёрное окно, нет отрисовки | Логика только в потоке, забыли repaint() |
После update вызывайте repaint() |
| Зависание при закрытии | System.exit без остановки потока |
running = false, interrupt потока |
| Враги не умирают | Нет break после попадания снаряда |
Один снаряд — один враг за шаг итератора |
| Level-up без паузы | Не проверяете upgradeState в начале update |
Ранний return при PAUSED_FOR_UPGRADE |
ConcurrentModificationException |
Удаление из списка во вложенном for |
Iterator.remove() |
| Кракозябры в HUD | CP1251 вместо UTF-8 | UTF-8 в IDE и project.build.sourceEncoding |
Идеи для расширения
- Мультивыстрел — поле
multishotMultiplierувеличивает число снарядов вshootMagicBolt(веер). - Сохранения —
SaveData+ObjectOutputStreamвsave.json(какSaveSystemв репозитории). - Персонажи —
CharacterDefсо стартовымWeaponTypeи ценой разблокировки в монетах. - Звук —
javax.sound.sampled, короткие WAV вassets/sfx/. - Порт на JavaFX или LibGDX — те же списки
enemies/projectiles, другой рендер.
Чек-лист "ощущение Vampire Survivors"
| Ощущение | Реализовано в практикуме? |
|---|---|
| Толпа нарастает со временем | Этапы 3, 15 (batch, difficulty) |
| Становишься сильнее быстрее врагов | Этапы 6–9 (XP + оружие) |
| Решения на level-up | Этап 7 |
| Одна ошибка — наказание | Этап 10 |
| "Ещё один ран" | Этап 13 |
| Визуальный шум (juice) | Этап 12 |
Связанные материалы
- Практикум разработки игр — о разделе — другие треки (Python, TypeScript).
- Java — о разделе — синтаксис, JVM, инструменты.
- Java Survivors на GitHub — полная версия практикума.
- TypeScript — TypeScript Survivors — тот же жанр в браузере (в подготовке).
- Игроведение — о разделе — жанры, механики, контекст индустрии.