«Танчики» на HTML5
Уничтожение врагов
Что ж. Намеченный план был выполнен полностью.
Было три итерации, сначала сделал так, чтоб танки могли крошить всех (в том числе и враги уничтожали друг друга).
Потом я запретил им убивать друг друга, а на третьем этапе ещё добавил взаимоуничтожение снарядов игрока и врага при попадании.
Давайте посмотрим, что там по коду интересного и не очень.
Из нового: появился файлик 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'ы и просто проверять их столкновения без отдельных методов (проверить ТанкТанк, проверить ТанкПуля, проверить ПуляПуля). В таком случае логичнее было бы добавить возможность делать коллизии триггерными, для отработки логики уже внутри самих объектов. Но в таком случае для такой простой игры значительно усложнилась бы архитектура. Поэтому в жертву универсальности я пошёл по более прямолинейному пути.
План на следующий этап:
Приговорён к пожизненному прослушиванию репетиций начинающих коллективов... Не каждый такое выдержит
Эволюция блин ))
Печально, иногда хочется что то новое интересное