Сообщества
GameDev
GameDev

GameDev

Постов 26
Подписчиков 4

О сообществе

Посты, посвящённые разработке игры

Разрешено

Писать про разработку игр

Запрещено

Писать не про разработку игр

«Танчики» на HTML5

Состояния игры и графика

0
Читалка

Изначально этим этапом должны были быть только состояния (начало пауза, геймовер), но так как два из трёх состояний я сделал ещё в прошлом этапе, то тут я добавил паузу, заготовку для главного меню и решил, что этого мало и перешёл сразу к следующему этапу, а именно замена временных квадратов на спрайты. Я решил не использовать то, что выдаёт там интернет, но постарался сам нарисовать близко по стилистике

По спецификациям спрайтов: сделал по два кадра на все танки, чтоб сделать анимацию гусениц. три кадра на воду. остальные по одному кадру

базовый враг
базовый враг
кусты лёд
кусты лёд
вода
вода

Вообще вот столько спрайтшитов отрисовал:

Результат сейчас выглядит вот так

В процессе записи понял, что забыл реализовать возрождение игрока после смерти :)

Для водички отдельная функция появилась:

export function updateWaterAnimation() {
  waterAnimationFrame = (waterAnimationFrame + 1) % WATER_SPRITE.frameCount;
}
//Ну и в рисовании фона вода теперь принимает непосредственное участие в отрисовки фона
renderBackground(gameMap, getWaterAnimationFrame());
//И внутри рендера ещё добавлен фолбек, чтобы в случае чего рисовать квадратики, как раньше.
//Чтоб не получилось что и-за какой-то ошибки на экране будет пустой квадрат
// Попробуем отрисовать спрайт
      const spriteDrawn = renderTileSprite(
        ctx, tileId, x, y, px, py, size,
        gameMap, wallsSprite, waterSprite, forestIceSprite, waterFrame
      );

      // Если спрайт не нарисован - fallback на цветной квадрат
      if (!spriteDrawn) {
        renderTileFallback(ctx, tileDef, x, y, px, py, size, gameMap);
      }

Конфигурация эффектов так же содержит фолбэк

/**
 * Конфигурация эффектов
 */
const EFFECT_CONFIG = {
    [EffectType.EXPLOSION_SMALL]: {
        maxFrames: EXPLOSION_SMALL_SPRITE.frameCount,  // 3 кадра
        frameTime: 3,  // 3 игровых кадра на кадр анимации (100ms при 30 FPS)
        size: TANK_SIZE,  // 16x16
        colors: ['#FFFF00', '#FFA500', '#FF4500']  // Fallback: Жёлтый → оранжевый → красный
    },
    [EffectType.EXPLOSION_BIG]: {
        maxFrames: EXPLOSION_BIG_SPRITE.frameCount,  // 4 кадра
        frameTime: 3,
        size: TANK_SIZE,  // 16x16
        colors: ['#FFFFFF', '#FFFF00', '#FFA500', '#FF4500']  // Fallback
    },
    [EffectType.SPAWN]: {
        maxFrames: SPAWN_SPRITE.frameCount,  // 5 кадров
        frameTime: 4,
        size: TANK_SIZE,  // 16x16
        colors: ['#FFFFFF', '#AAAAAA', '#FFFFFF', '#AAAAAA', '#FFFFFF']  // Fallback: Мигание
    }
};

Размер тут, правда, как будто излишен, так как все спрайты 16 на 16 и не пока не вижу необходимости делать другие, но посмотрю, может появился необходимость делать что-то больше чем 1 клетка размером, если нет, удалю.

Дальше по состояниям. В main.js появился метод и много его вызовов:

if (uiActions.pause) {
        setState(GameState.PAUSED);
}
export function setState(newState) {
    if (currentState === newState) return;

    const oldState = currentState;
    console.log(`State: ${oldState} -> ${newState}`);
    currentState = newState;
    stateTimer = 0;

    // Вызываем callback если установлен
    if (onStateChangeCallback) {
        onStateChangeCallback(newState, oldState);
    }
}

export function setOnStateChange(callback) {
    onStateChangeCallback = callback;
}

// Последний метод вызывается один раз при инициализации:
setOnStateChange(handleStateChange);

function handleStateChange(newState, oldState) {
  // При переходе из STAGE_COMPLETE в STAGE_INTRO (по таймеру) — инициализируем уровень
  if (oldState === GameState.STAGE_COMPLETE && newState === GameState.STAGE_INTRO) {
    initializeLevel(getCurrentStage());
  }
}
// это нам пригодится в будущем

План действий

«Танчики» на HTML5

Уничтожение базы

0
Читалка
Такой вот результат

Что сделано и как.

Ну во-первых появилась сущность Базы

class Base {
    constructor(x, y, gameMap = null) {
        this.x = x;
        this.y = y;
        this.width = BASE_SIZE;
        this.height = BASE_SIZE;

        // Состояние
        this.destroyed = false;

        // Временная защита бетоном (бонус "лопата")
        this.fortified = false;
        this.fortifyTimeLeft = 0;

        // Ссылка на карту
        this.gameMap = gameMap;
    }
}

Там на самом деле много служебных методов, типа рендера и получить границы. Но их сюда не пихаю, так как там ничего интересного. Есть ещё метод takeDamage, который просто удаляет визуальное отображение базы с экрана и помечает базу как уничтоженную.

В main.js появились такие нюансы:

for (const bullet of bullets) {
    if (!bullet.active) continue;

    bullet.update(gameMap, allTanks);

    // Создаём эффекты при попадании
    if (bullet.hitResult === 'base') {
      // Попадание в базу - большой взрыв и мгновенный Game Over
      if (base && !base.destroyed) {
        const baseCenterX = base.x + base.width / 2;
        const baseCenterY = base.y + base.height / 2;
        effects.push(createBigExplosion(baseCenterX, baseCenterY));

        base.takeDamage();
        gameOver = true;
      }
    } else if (bullet.hitResult === 'tank' && bullet.hitTarget) {
      // Попадание в танк - большой взрыв в центре танка
      ...
    } else if (bullet.hitResult === 'wall') {
      // Попадание в стену - маленький взрыв в позиции пули
      ...
    }
  }
//Не очень мне нравится это, скорее всего на финальном этапе полировки, перепишу

Внутри "коллизии" появился новый метод

export function checkBulletBaseCollision(bullet, base) {
    if (!base || base.destroyed) return false;

    const bulletBox = {
        x: bullet.x,
        y: bullet.y,
        width: BULLET_SIZE,
        height: BULLET_SIZE
    };
    const baseBox = base.getBounds();

    return checkAABBCollision(bulletBox, baseBox);
}

В оригинальной игре квадратики справа от игрового поля показывали, сколько танков ещё должно родиться, я решил сделать, чтоб отображалось сколько танков ещё осталось убить для завершения уровня. Ну а сам спавн сделан вот таким образом:

function updateEnemySpawn() {
    // Не спавним если игра окончена или уровень пройден
    if (gameOver || stageComplete) return;

    // Не спавним если больше нет врагов
    if (enemiesRemaining <= 0) return;

    // Не спавним если на экране максимум врагов
    if (enemies.length >= SpawnSettings.MAX_ON_SCREEN) return;

    // Уменьшаем таймер
    spawnCooldown--;

    // Если таймер истёк - спавним врага
    if (spawnCooldown <= 0) {
        // Получаем текущую точку спавна
        const spawnPoint = ENEMY_SPAWN_POINTS[spawnPointIndex];

        // Спавним врага
        spawnEnemy(spawnPoint.x, spawnPoint.y);

        // Уменьшаем счётчик оставшихся врагов
        enemiesRemaining--;

        // Переключаемся на следующую точку (по кругу)
        spawnPointIndex = (spawnPointIndex + 1) % ENEMY_SPAWN_POINTS.length;

        // Сбрасываем таймер
        spawnCooldown = SpawnSettings.SPAWN_COOLDOWN;

        // Обновляем ссылки на танки
        updateAllTanksReferences();
    }
}

Я, честно признаться, не помню как там оно было на дэнди, поэтому решил, что начиная с этого этапа уже не буду пытаться повторить танки какими они были на самом деле, а буду делать такими, какими я их помню/вижу в своей голове. Так что тут могут начаться расхождения с каноном. Но, учитывая, что я в итоге собираюсь сделать с игрой, это меньшее зло))

План на следующий этап
План на следующий этап

«Танчики» на HTML5

Уничтожение врагов

0
Читалка

Что ж. Намеченный план был выполнен полностью.

Было три итерации, сначала сделал так, чтоб танки могли крошить всех (в том числе и враги уничтожали друг друга).

Потом я запретил им убивать друг друга, а на третьем этапе ещё добавил взаимоуничтожение снарядов игрока и врага при попадании.

Давайте посмотрим, что там по коду интересного и не очень.

Из нового: появился файлик effects.js, который отвечает за всякие красивости, из самого заметного: взрывы)

const EFFECT_CONFIG = {
    [EffectType.EXPLOSION_SMALL]: {
        maxFrames: 3,
        frameTime: 3,  // 3 игровых кадра на кадр анимации (100ms при 30 FPS)
        size: BULLET_SIZE * 2,
        colors: ['#FFFF00', '#FFA500', '#FF4500']  // Жёлтый → оранжевый → красный
    },
    [EffectType.EXPLOSION_BIG]: {
        maxFrames: 5,
        frameTime: 3,
        size: TANK_SIZE * 1.5,
        colors: ['#FFFFFF', '#FFFF00', '#FFA500', '#FF4500', '#8B0000']  // Белый → жёлтый → оранжевый → красный → тёмно-красный
    },
    [EffectType.SPAWN]: {
        maxFrames: 4,
        frameTime: 4,
        size: TANK_SIZE,
        colors: ['#FFFFFF', '#888888', '#FFFFFF', '#888888']  // Мигание
    }
};

И сам класс эффекта:

export class Effect {
    /**
     * @param {number} x - Логическая координата X (центр эффекта)
     * @param {number} y - Логическая координата Y (центр эффекта)
     * @param {string} type - Тип эффекта (EffectType)
     */
    constructor(x, y, type = EffectType.EXPLOSION_SMALL) {
        this.x = x;
        this.y = y;
        this.type = type;

        // Получаем конфигурацию для этого типа эффекта
        const config = EFFECT_CONFIG[type] || EFFECT_CONFIG[EffectType.EXPLOSION_SMALL];

        this.maxFrames = config.maxFrames;
        this.frameTime = config.frameTime;
        this.size = config.size;
        this.colors = config.colors;

        // Состояние анимации
        this.frame = 0;
        this.age = 0;
    }

    /**
     * Обновление состояния эффекта
     * @param {number} dt - Время кадра (не используется при fixed timestep)
     */
    update(dt) {
        this.age++;

        // Переход к следующему кадру анимации
        if (this.age >= this.frameTime) {
            this.frame++;
            this.age = 0;
        }
    }

    /**
     * Проверка завершения анимации
     * @returns {boolean} true если анимация закончилась
     */
    isDone() {
        return this.frame >= this.maxFrames;
    }

    /**
     * Отрисовка эффекта
     */
    render() {
        if (this.isDone()) return;

        const ctx = foregroundCtx;

        // Конвертация логических координат в физические
        const px = GAME_FIELD_X + this.x * GAME_SCALE;
        const py = GAME_FIELD_Y + this.y * GAME_SCALE;

        // Размер эффекта меняется в зависимости от кадра
        const progress = this.frame / (this.maxFrames - 1);
        const currentSize = this.size * GAME_SCALE * (0.5 + progress * 0.5);

        // Цвет из массива цветов
        const colorIndex = Math.min(this.frame, this.colors.length - 1);
        const color = this.colors[colorIndex];

        // Рисуем взрыв как круг
        ctx.beginPath();
        ctx.arc(px, py, currentSize / 2, 0, Math.PI * 2);
        ctx.fillStyle = color;
        ctx.fill();

        // Внутренний круг (ядро взрыва)
        if (this.type === EffectType.EXPLOSION_BIG && this.frame < this.maxFrames - 1) {
            ctx.beginPath();
            ctx.arc(px, py, currentSize / 4, 0, Math.PI * 2);
            ctx.fillStyle = '#FFFFFF';
            ctx.fill();
        }
    }
}

Часть этого кода перепишется, когда я переведу игру на спрайты, оставив только смену кадров и "возраст", а все остальные визуальности удалив.

В файле коллизий появилась ещё пара новых методов:

export function checkBulletTankCollision(bullet, tanks) {
    const bulletBox = {
        x: bullet.x,
        y: bullet.y,
        width: BULLET_SIZE,
        height: BULLET_SIZE
    };

    // Определяем, является ли владелец пули врагом
    const ownerIsEnemy = bullet.owner && bullet.owner.type !== TankType.PLAYER;

    for (const tank of tanks) {
        // Пропускаем владельца пули и уничтоженных
        if (tank === bullet.owner || tank.destroyed) continue;

        // Вражеские пули пролетают сквозь других врагов (как в оригинале)
        if (ownerIsEnemy && tank.type !== TankType.PLAYER) continue;

        const tankBox = tank.getBounds();
        if (checkAABBCollision(bulletBox, tankBox)) {
            return tank; // Попадание в танк
        }
    }

    return null;
}

export function checkBulletBulletCollisions(bullets) {
    const collisions = [];

    for (let i = 0; i < bullets.length; i++) {
        const bullet1 = bullets[i];
        if (!bullet1.active) continue;

        const box1 = {
            x: bullet1.x,
            y: bullet1.y,
            width: BULLET_SIZE,
            height: BULLET_SIZE
        };

        // Определяем, принадлежит ли пуля игроку
        const isPlayer1 = bullet1.owner && bullet1.owner.type === TankType.PLAYER;

        for (let j = i + 1; j < bullets.length; j++) {
            const bullet2 = bullets[j];
            if (!bullet2.active) continue;

            const isPlayer2 = bullet2.owner && bullet2.owner.type === TankType.PLAYER;

            // Сталкиваются только пули разных команд (игрок vs враг)
            if (isPlayer1 === isPlayer2) continue;

            const box2 = {
                x: bullet2.x,
                y: bullet2.y,
                width: BULLET_SIZE,
                height: BULLET_SIZE
            };

            if (checkAABBCollision(box1, box2)) {
                collisions.push({ bullet1, bullet2 });
            }
        }
    }

    return collisions;
}

В целом, тут можно было бы выбрать и иные решения, коллизия и методы их проверки могли бы стать более универсальными, как это сделано, скажем, в Unity. Навесить на все объекты box collider'ы и просто проверять их столкновения без отдельных методов (проверить ТанкТанк, проверить ТанкПуля, проверить ПуляПуля). В таком случае логичнее было бы добавить возможность делать коллизии триггерными, для отработки логики уже внутри самих объектов. Но в таком случае для такой простой игры значительно усложнилась бы архитектура. Поэтому в жертву универсальности я пошёл по более прямолинейному пути.

План на следующий этап:

Ну и, наверное, если с базой быстро разберусь, добавлю автоспавн врагов, как в оригинальной игре.
Ну и, наверное, если с базой быстро разберусь, добавлю автоспавн врагов, как в оригинальной игре.

Всем хороших игр!

«Танчики» на HTML5

Примитивный ИИ врагов

0
Читалка

Всем причастных с праздником!

Я тут вспомнил, что в прошлом посте забыл добавить планы на будущее. Планами было: добавить врагов.

Соответственно: точки спавна, их перемещение по карте, стрельба.

Давайте посмотрим на результат.

Появился новый класс, отвечающий за поведение врагов

export class EnemyAI {
    constructor(tank) {
        this.tank = tank;

        // Таймер смены направления
        this.changeDirectionTimer = 0;
        this.changeDirectionInterval = 60; // ~2 сек при 30 FPS

        // Cooldown стрельбы
        this.shootCooldown = 0;
        this.shootInterval = 30; // ~1 сек при 30 FPS

        // Флаг застревания (для смены направления при коллизии)
        this.wasBlocked = false;
    }

    randomDirection() {
        const directions = [Direction.UP, Direction.RIGHT, Direction.DOWN, Direction.LEFT];
        return directions[Math.floor(Math.random() * 4)];
    }

    update(allTanks, gameMap, bullets) {
        const tank = this.tank;

        if (tank.destroyed) return;

        // === 1. Логика смены направления ===
        this.changeDirectionTimer++;

        // Смена направления по таймеру
        if (this.changeDirectionTimer >= this.changeDirectionInterval) {
            tank.direction = this.randomDirection();
            this.changeDirectionTimer = 0;
            // Рандомизируем интервал (40-80 кадров)
            this.changeDirectionInterval = 40 + Math.floor(Math.random() * 40);
        }

        // === 2. Попытка движения ===
        const prevX = tank.x;
        const prevY = tank.y;

        tank.move(tank.direction);

        // Проверка: застрял ли танк (не сдвинулся)
        const stuck = (tank.x === prevX && tank.y === prevY && !tank.moving);

        if (stuck && !this.wasBlocked) {
            // Танк только что застрял — меняем направление
            tank.direction = this.randomDirection();
            this.wasBlocked = true;
            this.changeDirectionTimer = 0;
        } else if (!stuck) {
            this.wasBlocked = false;
        }

        // === 3. Автоматическая стрельба ===
        this.shootCooldown--;

        if (this.shootCooldown <= 0 && tank.canShoot()) {
            const bullet = tank.shoot();
            if (bullet) {
                bullets.push(bullet);
                // Рандомизируем cooldown (20-50 кадров)
                this.shootCooldown = 20 + Math.floor(Math.random() * 30);
            }
        }
    }
}

Он простой как три рубля, так что, думаю, можно обойтись без объяснений (тем более что комментариев в коде и так предостаточно).

К файле main появились коллекции врагов и их "мозгов"

/** @type {Array<Tank>} */
let enemies = [];

/** @type {Array<EnemyAI>} */
let enemyAIs = [];

Спавн врагов при старте игры

spawnEnemy(0, 0);           // Левый угол
spawnEnemy(12 * 8, 0);      // Центр (96px)
spawnEnemy(24 * 8 - 16, 0); // Правый угол (176px, учитываем размер танка)
// Пока спавню сразу все три, а не по очереди, как в оригинале
function spawnEnemy(x, y) {
    const enemy = new Tank(x, y, TankType.ENEMY_BASIC);
    enemy.direction = Direction.DOWN; // Враги всегда смотрят вниз
    enemy.setMap(gameMap);

    // Создаём AI для врага
    const ai = new EnemyAI(enemy);

    enemies.push(enemy);
    enemyAIs.push(ai);

    console.log(`Enemy spawned at (${x}, ${y})`);
}

И рисуем это

for (const enemy of enemies) {
    if (!enemy.destroyed) {
      enemy.render();
    }
  }

Там ещё есть обновление ссылок чтоб чистить из памяти уничтоженные танки, но в целом ничего больше интересного.

Оптимистичный план на следующий этап вот такой:

Всем хороших игр!

«Танчики» на html5

Реакция на попадание снаряда

0
Читалка
Внезапно сразу результат

А теперь давайте посмотрим, что изменилось.

В первую очередь, конечно же, изменился метод checkTileCollision внутри класс патрона.

checkTileCollision(newX, newY, gameMap) {
        const tiles = getTilesUnderEntity(newX, newY, BULLET_SIZE, BULLET_SIZE);

        if (tiles.length === 0) return false;

        // Находим тайлы на передней кромке по направлению движения
        let frontTiles;
        switch (this.direction) {
            case Direction.UP:
                // Минимальный Y = передняя кромка
                const minY = Math.min(...tiles.map(t => t.ty));
                frontTiles = tiles.filter(t => t.ty === minY);
                break;
            case Direction.DOWN:
                // Максимальный Y = передняя кромка
                const maxY = Math.max(...tiles.map(t => t.ty));
                frontTiles = tiles.filter(t => t.ty === maxY);
                break;
            case Direction.LEFT:
                // Минимальный X = передняя кромка
                const minX = Math.min(...tiles.map(t => t.tx));
                frontTiles = tiles.filter(t => t.tx === minX);
                break;
            case Direction.RIGHT:
                // Максимальный X = передняя кромка
                const maxX = Math.max(...tiles.map(t => t.tx));
                frontTiles = tiles.filter(t => t.tx === maxX);
                break;
            default:
                frontTiles = tiles;
        }

        let hasCollision = false;

        // Проверяем только тайлы на передней кромке
        for (const {tx, ty} of frontTiles) {
            const tileId = gameMap.getTile(tx, ty);
            const tileDef = TILE_DEFS[tileId];

            if (!tileDef) continue;

            // Пуля блокируется этим тайлом?
            if (tileDef.blocksBullet) {
                hasCollision = true;

                // Пытаемся разрушить тайл
                gameMap.damageTile(tx, ty, this.direction, this.power);
            }
        }

        if (hasCollision) {
            this.destroy();
            return true;
        }

        return false;
    }

Изменился внутренний мир объекта, отвечающего за направления

DamageState = Object.freeze({
//было
    FULL: 0b11,
    HALF_LEFT: 0b10,
    HALF_RIGHT: 0b01,
    DESTROYED: 0b00
//стало
    FULL: 0,
    HALF_LEFT: 1,   // Осталась левая половина (вертикальный срез)
    HALF_RIGHT: 2,  // Осталась правая половина (вертикальный срез)
    HALF_TOP: 3,    // Осталась верхняя половина (горизонтальный срез)
    HALF_BOTTOM: 4, // Осталась нижняя половина (горизонтальный срез)
    DESTROYED: 5
});

Эта побитовая маска оказалась весьма нестабильной историей на тестах.

Ну и как говорил в прошлом выпуске, мне не нравилось, как у меня был сделан инпут, поэтому я его переделал. Он больше не принимает на вход карту и коллекцию патронов, а делает только то, что должен: отслеживает инпут. Логика обработки вынесена уже в main.js

export function getPlayerActions() {
    const actions = {
        movement: null,  // Direction.UP/DOWN/LEFT/RIGHT или null
        shoot: false
    };

    // Собираем ввод с клавиатуры
    collectKeyboardActions(actions);

    // Собираем ввод с геймпада
    collectGamepadActions(actions);

    return actions;
}
function update(dt) {
  // 1. Получаем действия игрока из input
  const actions = getPlayerActions();

  // 2. Применяем действия к игроку
  if (player && !player.destroyed) {
    // Движение
    if (actions.movement !== null) {
      player.move(actions.movement);
    }

    // Стрельба
    if (actions.shoot && player.canShoot()) {
      const bullet = player.shoot();
      if (bullet) {
        bullets.push(bullet);
      }
    }

    player.update(dt);
  }

  // 3. Обновление пуль
  for (const bullet of bullets) {
    bullet.update(gameMap);
  }

  // 4. Удаление неактивных пуль
  bullets = bullets.filter(b => b.active);

  // 5. Подсчёт FPS для отладки
  frameCount++;
  fpsTimer += dt;

  if (fpsTimer >= 1000) {
    fps = frameCount;
    frameCount = 0;
    fpsTimer = 0;

    // Обновляем FPS на странице
    const fpsElement = document.getElementById('fps');
    if (fpsElement) {
      fpsElement.textContent = fps;
    }
  }
}

Внутри карты поменялось получение и редактирование карты урона

getDamage(x, y) {
    const key = `${x},${y}`;
    // Если нет в маске — значит либо FULL, либо не разрушаемый
    if (!this.damageMask.has(key)) {
      return DamageState.FULL;
    }
    return this.damageMask.get(key);
  }
setDamage(x, y, state) {
    const key = `${x},${y}`;
    if (state === DamageState.DESTROYED) {
      this.damageMask.delete(key);
      this.tileGrid[y][x] = TileType.EMPTY;
    } else if (state === DamageState.FULL) {
      // FULL — удаляем из маски (дефолтное состояние)
      this.damageMask.delete(key);
    } else {
      this.damageMask.set(key, state);
    }
    this.dirty = true;
  }

И появился большой новый метод, который и занимается непосредственно разрушением:

damageTile(x, y, direction, power) {
    if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return false;

    const tileId = this.getTile(x, y);
    const tileDef = TILE_DEFS[tileId];

    if (!tileDef) return false;

    // Проверяем можно ли разрушить этот тайл
    if (!canBulletDestroy(tileDef, power)) {
      return false;
    }

    // Получаем текущее состояние повреждения
    const currentDamage = this.getDamage(x, y);

    // Если уже разрушен — ничего не делаем
    if (currentDamage === DamageState.DESTROYED) {
      return false;
    }

    // Определяем новое состояние в зависимости от направления пули
    let newDamage;

    if (currentDamage === DamageState.FULL) {
      // Первое попадание — создаём половину
      // Пуля убирает ту часть, в которую летит
      switch (direction) {
        case Direction.UP:
          // Пуля летит вверх → убирает нижнюю часть → остаётся верхняя
          newDamage = DamageState.HALF_TOP;
          break;
        case Direction.DOWN:
          // Пуля летит вниз → убирает верхнюю часть → остаётся нижняя
          newDamage = DamageState.HALF_BOTTOM;
          break;
        case Direction.LEFT:
          // Пуля летит влево → убирает правую часть → остаётся левая
          newDamage = DamageState.HALF_LEFT;
          break;
        case Direction.RIGHT:
          // Пуля летит вправо → убирает левую часть → остаётся правая
          newDamage = DamageState.HALF_RIGHT;
          break;
      }
    } else {
      // Второе попадание — полное разрушение
      newDamage = DamageState.DESTROYED;
    }

    // Обновляем состояние
    this.setDamage(x, y, newDamage);

    return true;
  }

Пока складывается впечатление, что начинаю наворачивать мрак и ужас. Поэтому со следующего выпуска начну помимо введения новых фишек смотреть, что можно упростить и улучшить по архитектуре и коду. Как я уже в этот раз сделал с системой ввода.


Всем хороших игр!

Клон Divinity на Unity

3. Перемещаем персонажа

0

Начал переносить видео на фоксфрейм. Качество в результате переноса пострадало, но лучше так, чем никак...