3.6: Рух ворога
Що ми будемо робити?
У цьому підрозділі ми оновимо клас Enemy.js
, додавши базовий штучний інтелект для руху ворога.
Оновлення класу Enemy.js
Оновіть файл Enemy.js
:
javascript
import { Tank } from './Tank.js';
import { logger } from './main.js';
/**
* 🎮 Клас Enemy - представляє ворожого танка
*
* Відповідає за:
* - Логіку ворожого танка
* - Штучний інтелект
* - Рух та поведінку
*/
export class Enemy extends Tank {
constructor(options = {}) {
// Викликаємо конструктор батьківського класу Tank
super({
...options, // передаємо всі опції батьківському класу
// червоний колір за замовчуванням
color: options.color || '#e74c3c',
// ворог рухається повільніше за гравця
speed: options.speed || 1,
// початковий напрямок дула вниз
direction: options.direction || 'down'
});
// Межі руху (розміри Canvas)
this.bounds = {
minX: 0,
minY: 0,
maxX: 800,
maxY: 600
};
// Штучний інтелект
this.ai = {
// Поточний стан AI
state: 'patrol', // 'patrol', 'chase', 'attack', 'retreat'
// Налаштування патрулювання
patrol: {
targetX: this.x,
targetY: this.y,
changeDirectionTime: 3000, // 3 секунди
lastDirectionChange: 0
},
// Налаштування переслідування
chase: {
target: null, // ціль для переслідування
detectionRange: 150, // радіус виявлення
maxChaseTime: 10000 // 10 секунд переслідування
},
// Налаштування атаки
attack: {
attackRange: 100, // дистанція атаки
attackCooldown: 2000, // 2 секунди між атаками
lastAttackTime: 0
},
// Таймери
timers: {
stateChange: 0,
directionChange: 0
}
};
// Стан руху
this.movementState = {
isMoving: false,
lastDirection: 'down'
};
// записуємо в лог
logger.enemyAction('Ворог створений', `позиція: (${this.x}, ${this.y})`);
}
/**
* Встановлення меж руху
* @param {Object} bounds - Межі руху
*/
setBounds(bounds) {
this.bounds = { ...this.bounds, ...bounds };
}
/**
* Встановлення цілі для переслідування
* @param {Object} target - Ціль (гравець)
*/
setTarget(target) {
this.ai.chase.target = target;
}
/**
* Оновлення стану ворога
* @param {number} deltaTime - Час з останнього оновлення
*/
update(deltaTime) {
if (!this.isAlive) return;
// Оновлюємо таймери
this.updateTimers(deltaTime);
// Оновлюємо AI
this.updateAI(deltaTime);
// Оновлюємо рух
this.updateMovement(deltaTime);
}
/**
* Оновлення таймерів
* @param {number} deltaTime - Час з останнього оновлення
*/
updateTimers(deltaTime) {
this.ai.timers.stateChange += deltaTime;
this.ai.timers.directionChange += deltaTime;
this.ai.patrol.lastDirectionChange += deltaTime;
this.ai.attack.lastAttackTime += deltaTime;
}
/**
* Оновлення штучного інтелекту
* @param {number} deltaTime - Час з останнього оновлення
*/
updateAI(deltaTime) {
const target = this.ai.chase.target;
if (target && this.isTargetInRange(target, this.ai.chase.detectionRange)) {
// Ціль виявлена - переходимо в режим переслідування
if (this.ai.state !== 'chase') {
this.changeAIState('chase');
}
// Перевіряємо чи ціль в зоні атаки
if (this.isTargetInRange(target, this.ai.attack.attackRange)) {
this.changeAIState('attack');
}
} else {
// Ціль не виявлена - повертаємося до патрулювання
if (this.ai.state !== 'patrol') {
this.changeAIState('patrol');
}
}
// Зміна напрямку при патрулюванні
if (this.ai.state === 'patrol' &&
this.ai.patrol.lastDirectionChange >= this.ai.patrol.changeDirectionTime) {
this.changePatrolDirection();
}
}
/**
* Зміна стану AI
* @param {string} newState - Новий стан
*/
changeAIState(newState) {
if (this.ai.state === newState) return;
this.ai.state = newState;
this.ai.timers.stateChange = 0;
logger.enemyAction(`Ворог змінив стан на: ${newState}`);
switch (newState) {
case 'patrol':
this.setPatrolTarget();
break;
case 'chase':
// Починаємо переслідування
break;
case 'attack':
// Готові до атаки
break;
}
}
/**
* Оновлення руху ворога
* @param {number} deltaTime - Час з останнього оновлення
*/
updateMovement(deltaTime) {
let newX = this.x;
let newY = this.y;
let isMoving = false;
switch (this.ai.state) {
case 'patrol':
// Патрулювання - рух до цілі
const patrolTarget = this.ai.patrol;
if (this.x < patrolTarget.targetX - 5) {
newX += this.speed;
this.direction = 'right';
isMoving = true;
} else if (this.x > patrolTarget.targetX + 5) {
newX -= this.speed;
this.direction = 'left';
isMoving = true;
} else if (this.y < patrolTarget.targetY - 5) {
newY += this.speed;
this.direction = 'down';
isMoving = true;
} else if (this.y > patrolTarget.targetY + 5) {
newY -= this.speed;
this.direction = 'up';
isMoving = true;
}
break;
case 'chase':
// Переслідування гравця
if (this.ai.chase.target) {
const target = this.ai.chase.target;
const dx = target.x - this.x;
const dy = target.y - this.y;
// Рухаємося до гравця
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 0) {
newX += this.speed;
this.direction = 'right';
} else {
newX -= this.speed;
this.direction = 'left';
}
} else {
if (dy > 0) {
newY += this.speed;
this.direction = 'down';
} else {
newY -= this.speed;
this.direction = 'up';
}
}
isMoving = true;
}
break;
case 'attack':
// Атака - зупиняємося і стріляємо
isMoving = false;
break;
}
// Перевіряємо межі руху
if (this.checkBounds(newX, newY)) {
this.x = newX;
this.y = newY;
} else {
// Якщо вийшли за межі, змінюємо напрямок
this.changePatrolDirection();
}
// Оновлюємо стан руху
this.movementState.isMoving = isMoving;
if (isMoving) {
this.movementState.lastDirection = this.direction;
}
}
/**
* Перевірка чи ціль в діапазоні
* @param {Object} target - Ціль
* @param {number} range - Діапазон
* @returns {boolean} - true якщо ціль в діапазоні
*/
isTargetInRange(target, range) {
if (!target) return false;
const dx = target.x - this.x;
const dy = target.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= range;
}
/**
* Встановлення цілі патрулювання
*/
setPatrolTarget() {
// Випадкова позиція в межах поля
this.ai.patrol.targetX = Math.random() * (this.bounds.maxX - this.bounds.minX - 100) + this.bounds.minX + 50;
this.ai.patrol.targetY = Math.random() * (this.bounds.maxY - this.bounds.minY - 100) + this.bounds.minY + 50;
}
/**
* Зміна напрямку патрулювання
*/
changePatrolDirection() {
const directions = ['up', 'down', 'left', 'right'];
const randomDirection = directions[Math.floor(Math.random() * directions.length)];
this.direction = randomDirection;
this.ai.patrol.lastDirectionChange = 0;
// Встановлюємо нову ціль патрулювання
this.setPatrolTarget();
logger.enemyAction('Ворог змінив напрямок патрулювання', `новий напрямок: ${randomDirection}`);
}
/**
* Перевірка меж руху
* @param {number} newX - Нова X координата
* @param {number} newY - Нова Y координата
* @returns {boolean} - true якщо позиція в межах
*/
checkBounds(newX, newY) {
return newX >= this.bounds.minX &&
newX + this.width <= this.bounds.maxX &&
newY >= this.bounds.minY &&
newY + this.height <= this.bounds.maxY;
}
/**
* Малювання ворога на екрані
* @param {CanvasRenderingContext2D} ctx - Контекст для малювання
*/
render(ctx) {
// якщо ворог мертвий, не малюємо
if (!this.isAlive) return;
// зберігаємо поточний стан контексту (колір, стиль тощо)
ctx.save();
// викликаємо метод render батьківського класу
super.render(ctx);
// малюємо червоний хрестик
this.drawEnemyMark(ctx);
// малюємо індикатор стану AI
this.drawAIStateIndicator(ctx);
// відновлюємо стан контексту (повертаємо попередні налаштування)
ctx.restore();
}
/**
* Малювання позначки ворога (червоний хрестик)
* @param {CanvasRenderingContext2D} ctx - Контекст для малювання
*/
drawEnemyMark(ctx) {
// розмір позначки в пікселях
const markSize = 6;
// центр танка по X
const centerX = this.x + this.width / 2;
// центр танка по Y
const centerY = this.y + this.height / 2;
// темно-червоний колір для ліній
ctx.strokeStyle = '#c0392b';
// товщина ліній хрестика
ctx.lineWidth = 2;
// починаємо малювати шлях
ctx.beginPath();
// початкова точка
ctx.moveTo(centerX - markSize, centerY - markSize);
// кінцева точка
ctx.lineTo(centerX + markSize, centerY + markSize);
// малюємо лінію
ctx.stroke();
// починаємо малювати новий шлях
ctx.beginPath();
// початкова точка
ctx.moveTo(centerX + markSize, centerY - markSize);
// кінцева точка
ctx.lineTo(centerX - markSize, centerY + markSize);
// малюємо лінію
ctx.stroke();
}
/**
* Малювання індикатора стану AI
* @param {CanvasRenderingContext2D} ctx - Контекст для малювання
*/
drawAIStateIndicator(ctx) {
// Кольори для різних станів
const stateColors = {
patrol: '#3498db', // синій
chase: '#f39c12', // помаранчевий
attack: '#e74c3c', // червоний
retreat: '#9b59b6' // фіолетовий
};
const color = stateColors[this.ai.state] || '#95a5a6';
const indicatorSize = 3;
// Розміщуємо індикатор в правому верхньому куті танка
const indicatorX = this.x + this.width - indicatorSize - 2;
const indicatorY = this.y + 2;
// малюємо маленький квадрат
ctx.fillStyle = color;
ctx.fillRect(indicatorX, indicatorY, indicatorSize, indicatorSize);
}
/**
* Отримання стану AI
* @returns {Object} - Стан AI
*/
getAIState() {
return {
state: this.ai.state,
isMoving: this.movementState.isMoving,
direction: this.direction
};
}
}
Що додано до класу Enemy?
Нові властивості:
bounds
- межі руху (розміри Canvas)ai.state
- поточний стан AI ('patrol', 'chase', 'attack')ai.patrol
- налаштування патрулюванняai.chase
- налаштування переслідуванняai.attack
- налаштування атакиmovementState
- стан руху
Нові методи:
setBounds()
- встановлення меж рухуsetTarget()
- встановлення цілі для переслідуванняupdateAI()
- оновлення штучного інтелектуchangeAIState()
- зміна стану AIupdateMovement()
- оновлення рухуisTargetInRange()
- перевірка діапазону до ціліsetPatrolTarget()
- встановлення цілі патрулюванняchangePatrolDirection()
- зміна напрямку патрулюванняdrawAIStateIndicator()
- індикатор стану AI
Стани штучного інтелекту
1. Патрулювання (patrol
):
- Випадковий рух по полю
- Зміна напрямку кожні 3 секунди
- Випадкові цілі для руху
2. Переслідування (chase
):
- Рух до гравця при виявленні
- Радіус виявлення 150 пікселів
- Максимальний час переслідування 10 секунд
3. Атака (attack
):
- Зупинка для стрільби
- Дистанція атаки 100 пікселів
- Затримка між атаками 2 секунди
Візуальні індикатори
Індикатор стану AI:
- Синій - патрулювання
- Помаранчевий - переслідування
- Червоний - атака
- Фіолетовий - відступ
Розташування:
- Правий верхній кут танка
- Розмір 3x3 пікселі
Використання
javascript
// Створення ворога
const enemy = new Enemy({
x: 300,
y: 200,
color: '#e74c3c',
size: 32
});
// Встановлення меж руху
enemy.setBounds({
maxX: 800,
maxY: 600
});
// Встановлення цілі для переслідування
enemy.setTarget(player);
// Оновлення в ігровому циклі
enemy.update(deltaTime);
// Отримання стану AI
const aiState = enemy.getAIState();
console.log('Стан ворога:', aiState.state);
Результат
Після оновлення цього класу у вас буде:
- ✅ Базовий штучний інтелект ворога
- ✅ Патрулювання по полю
- ✅ Переслідування гравця
- ✅ Візуальні індикатори стану
- ✅ Готовність для стрільби ворога
Що далі?
У наступному підрозділі ми додамо стрільбу ворога з автоматичним націлюванням.