«Танчики» на html5
Стрельба | Снаряды
В прошлой серии вы видели: стены более непротицаемы.
В этой серии мы научим наш так шмалять белыми квадратами по сторонам! И будем убеждать себя, что это снаряды.
В первую очередь мы, получается, должны создать сущность снаряда. Оборачиваем это безобразие в класс:
export class Bullet {
constructor(x, y, direction, owner, power = 0) {
this.x = x;
this.y = y;
this.direction = direction;
this.speed = Speed.BULLET;
this.power = power;
this.owner = owner;
// Состояние
this.active = true;
}
}
Сущность пули помнит, кто её владелец, знает свою скорость и мощность. Мощность нам понадобится на следующих этапах. Например, стандартная пуля может только «брить» кирпичи, более мощная сможет уничтожать уже и бетонные/стальные блоки. В каких-то версиях пиратских танков была возможность даже сбривать кусты.
Далее у пули есть служебные методы, которые нужны для просчитывания коллизий, отрисовки положения в пространстве и т.д.
update(gameMap) {
if (!this.active) return;
// Вычисляем смещение
let dx = 0;
let dy = 0;
switch (this.direction) {
case Direction.UP:
dy = -this.speed;
break;
case Direction.DOWN:
dy = this.speed;
break;
case Direction.LEFT:
dx = -this.speed;
break;
case Direction.RIGHT:
dx = this.speed;
break;
}
// Новая позиция
const newX = this.x + dx;
const newY = this.y + dy;
// Проверка границ карты
if (newX < 0 || newX + BULLET_SIZE > LOGICAL_FIELD_SIZE ||
newY < 0 || newY + BULLET_SIZE > LOGICAL_FIELD_SIZE) {
this.destroy();
return;
}
// Проверка коллизий с тайлами
if (gameMap && this.checkTileCollision(newX, newY, gameMap)) {
return; // Пуля уничтожена в checkTileCollision
}
this.x = newX;
this.y = newY;
}
checkTileCollision(newX, newY, gameMap) {
const tiles = getTilesUnderEntity(newX, newY, BULLET_SIZE, BULLET_SIZE);
for (const {tx, ty} of tiles) {
const tileId = gameMap.getTile(tx, ty);
const tileDef = TILE_DEFS[tileId];
if (!tileDef) continue;
// Пуля блокируется этим тайлом?
if (tileDef.blocksBullet) {
this.destroy();
// TODO: Разрушение тайла (на следующем этапе)
return true;
}
}
return false;
}
destroy() {
if (!this.active) return;
this.active = false;
if (this.owner) {
this.owner.activeBullets = Math.max(0, this.owner.activeBullets - 1);
}
// TODO: Создание эффекта взрыва (Не скоро, ещё этапа через три)
}
render() {
if (!this.active) return;
const ctx = foregroundCtx;
// Конвертация логических координат в физические
const px = GAME_FIELD_X + this.x * GAME_SCALE;
const py = GAME_FIELD_Y + this.y * GAME_SCALE;
const size = BULLET_SIZE * GAME_SCALE;
// Рисуем пулю как белый квадрат
ctx.fillStyle = Colors.BULLET;
ctx.fillRect(px, py, size, size);
}
getBounds() {
return {
x: this.x,
y: this.y,
width: BULLET_SIZE,
height: BULLET_SIZE
};
}
В самом танке обновляем метод стрельбы. Раньше там не было нифига, теперь вот:
shoot() {
if (this.activeBullets >= this.bulletCount) {
return null;
}
//return null; - так было
// так стало:
// Вычисляем позицию пули (центр передней части танка)
let bulletX, bulletY;
const centerOffset = (TANK_SIZE - BULLET_SIZE) / 2;
switch (this.direction) {
case Direction.UP:
bulletX = this.x + centerOffset;
bulletY = this.y - BULLET_SIZE;
break;
case Direction.DOWN:
bulletX = this.x + centerOffset;
bulletY = this.y + TANK_SIZE;
break;
case Direction.LEFT:
bulletX = this.x - BULLET_SIZE;
bulletY = this.y + centerOffset;
break;
case Direction.RIGHT:
bulletX = this.x + TANK_SIZE;
bulletY = this.y + centerOffset;
break;
}
// Создаём пулю
const bullet = new Bullet(bulletX, bulletY, this.direction, this, this.bulletLevel);
this.activeBullets++;
return bullet;
}
Изменилась обработка пользовательского ввода
function processInput(player, gameMap = null, bullets = []) {
if (!player || player.destroyed) return;
// Обрабатываем клавиатуру
processKeyboardInput(player, gameMap, bullets);
// Обрабатываем геймпад
processGamepadInput(player, gameMap, bullets);
}
И вот тут я прям серьёзно задумался: «А не хуйню ли я делаю?». Как будто передавать карту и массив снарядов в обработку инпутов – гавно идея...
К следующему посту поправлю.
А пока вот так:
if (keys['Space'] || keys['Enter']) {
if (player.canShoot()) {
const bullet = player.shoot();
if (bullet) {
bullets.push(bullet);
}
}
}
В основном методе update теперь вызываем и апдейт всех пуль
for (const bullet of bullets) {
bullet.update(gameMap);
}
И в основном рендере их рисуем
// Рендер пуль
for (const bullet of bullets) {
bullet.render();
}
Если есть идеи по улучшению кода, рад буду почитать
План на следующий этап
Всем хороших книг!
newX < 0 || newX + BULLET_SIZE > LOGICAL_FIELD_SIZE
Если координаты изменяются от 0 включительно до LOGICAL_FIELD_SIZE включительно, то реальный размер поля LOGICAL_FIELD_SIZE+1. Короче в проверках ошибка. Кстати, типичная для программистов, т.к. есть элемент с индексом 0
newX < 0 || newX + BULLET_SIZE > LOGICAL_FIELD_SIZE
Если координаты изменяются от 0 включительно до LOGICAL_FIELD_SIZE включительно, то реальный размер поля LOGICAL_FIELD_SIZE+1. Короче в проверк...
Ошибка типичная, но здесь ошибки нет. Здесь чистая матаматика. Размер поля 208, то есть от 0 до 207. Размер патрона 4. Координата новой позиции пляшет от верхнего левого угла. Если координата, скажем, 204, то 204+4 = 208 – это и есть размер поля. Смещаемся ещё хоть на один пиксель, выходим за края поля.
Комментарий