Простейший AI на примере мини-игры (часть 1)
Автор: Ерёмин Андрей
Большинство компьютерных игр содержат содержат искусственный интеллект. Если брать во внимание все серьёзные игры (Action, RPG и т.п.), то они полностью "захвачены искусственным разумом". Другое дело - мини-игры. К примеру, общеизвестный Сапёр прекрасно живёт и без интеллекта, думать ему во время игры вообще не нужно... Да что говорить и самой игре, если в некоторых ситуациях мыслительный процесс самого игрока ни к чему не приведёт - бывают ситуации, когда нужно просто щёлкнуть наугад - тут уже вероятность 50% - либо попал на мину, либо не попал... :-)
Проблема искусственного интеллекта известна довольно давно. Противостояние игроку - одна из целей. Действительно, не интересно было бы играть, если бы монстры вместо того, чтобы нападать на вас, ходили бы в разные стороны только из-за того, что направление движения выбирается с помощью случайных чисел, а пойти с атакой на конкретную точку они не догадываются. Каждая конкретная игра требует своего интеллекта, который является уникальным. Интеллект в одной игре не применим в другой.
Сейчас мы попытаемся создать самый простой AI (Artificial Intelligence кстати) на примере небольшой игры.
Игра
Давайте создадим игру, которую можно назвать "Догонялки". Задача игрока очень проста: управляя своим героем, стараться не попасть в лапы соперника. Очень простая задумка, но тем не менее здесь будет AI, хотя и простейший. Пусть программа автоматически увеличивает уровень сложности по мере игры.
Проектируем интерфейс
Сделаем игру на основе стандартных компонент. Не будем применять никакой графики, ибо речь совсем не об этом. Итак, пусть наши "герои" будут простыми... квадратами. А почему бы и нет? Нет у нас времени на рисование персонажей - всё будет абстрактно. Размещаем на форме 2 компонента TShape (вкладка Additional палитры компонент). Один сделаем красным, а другой синим. Цвет заливки задаётся свойством Brush - Color. Пусть наш герой будет синим, а враг - красным. С персонажами определились. Что нам ещё нужно? Наверное, кнопка для запуска игры. Поместите на форму кнопку, лучше в левый верхний угол. Ну и ещё разместим где-нибудь 3 текстовые метки (TLabel) - одна будет показывать время игры, другая - уровень сложности, а в третьей будет появляться информация о результатах.
Время игры
Чтобы участники могли сравнить свои результаты, игра будет выдавать итоговое время. Для начала объявляем глобальную переменную, в которой будем хранить количество секунд, в течение которых длится игра. В раздел var модуля добавляем: Time: Integer = 0; Теперь нам нужен способ отсчитывать секунды. Для этого используем TTimer (вкладка System). Interval пусть останется стандартным (1000 мс = 1 сек), а вот сам таймер мы изначально выключим: Enabled = False. Назовём этот таймер не Timer1, а просто Timer. Теперь пишем его обработчик события OnTimer:
procedure TForm1.TimerTimer(Sender: TObject);
begin
Inc(Time);
if Time < 60 then
TimeLabel.Caption:=IntToStr(Time)+' сек.'
else
TimeLabel.Caption:=IntToStr(Time div 60)+' мин. '+IntToStr(Time mod 60)+' сек.';
end;
Что же здесь происходит? Сначала мы прибавляем секунду к текущему времени. Затем в одну из текстовых меток, которая называется TimeLabel, выводим текущее время игры. Проверяем: если ещё не прошло одной минуты, то выводим просто секунды, а иначе выводим и минуты и секунды. Записать это можно немного по-другому, но не суть важно. Время почти готово, только в обработчик нажатия кнопки "Старт" нужно добавить включение игрового таймера: GameTimer.Enabled:=True; Вот теперь время точно готово.
Управление персонажем
Сделаем управление мышью для нашего "персонажа". Создаём обработчик на событие OnMouseMove формы, где устанвливаем фигурку в положение курсора:
procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
Player.Left:=X-Round(Player.Width/2);
Player.Top:=Y-Round(Player.Height/2);
end;
Координаты дополнительно изменяются на половину ширины и высоты фигурки, чтобы она оказалась точно по центру курсора.
Можно изменить курсор со стрелки на крестик - установить свойство Cursor в crCross для формы и для фигурки.
Ну вот, управление готово. Можно запустить программу и посмотреть, что получилось.
Интеллект
Для постоянной активности противника также будем использовать таймер. Назовём его GameTimer, зададим интервал в 0.1 с (Interval = 100) и выключим (Enabled = False). Назначение этого таймера в следующем: когда его событие будет активироваться, будет анализироваться положение врага и враг будет двигаться по направлению к игроку. Пусть Player - фигурка игрока, Enemy - фигурка противника. Для начала зададим шаг движения противника в виде количества точек, на которые будет смещаться фигурка. Объявляем глобальную переменную: Step: Byte = 10; Заодно заведём переменную и для текущего уровня сложности, который будет представлен цифрой: Level: Byte = 1;
Теперь разбираемся с интеллектом. Вот черновой вариант:
if Enemy.Left+Enemy.Width <= Player.Left then
Enemy.Left:=Enemy.Left+Step
else if Enemy.Left >= Player.Left+Player.Width then
Enemy.Left:=Enemy.Left-Step
else if Enemy.Top+Enemy.Height <= Player.Top then
Enemy.Top:=Enemy.Top+Step
else if Enemy.Top >= Player.Top+Player.Height then
Enemy.Top:=Enemy.Top-Step
else
Игрок пойман
Здесь мы сравниваем позиции игрока и противника и соответствующим образом перемещаем фигурку противника. Для определения "наложения" одной фигурки на другую будем сравнивать не просто координаты левого верхнего угла (Left и Top), а также будем прибавлять к ним размеры самих фигурок, т.к. они достаточно большие.
Итак, по порядку:
- если противник расположен левее игрока, то сдвигаем противника вправо;
- если противник правее игрока, сдвигаем его влево;
- если противник выше игрока, то сдвигаем вниз;
- если противник ниже, то сдвигаем вверх.
Достаточно простой алгоритм, который будет работать. Добавим в обработчик кнопки "Старт" запуск таймера для движения противника: GameTimer.Interval:=100; и GameTimer.Enabled:=True; Также нужно указать что-либо для выполнения в том случае, если враг не сдвинулся с места, т.е. участник пойман и проиграл. Например, можно добавить сообщение: ShowMessage('Вы проиграли!'); Запускаем программу и смотрим. Работает? Работает. Однако движение противника слишком определено: сначала он движется по горизонтали до тех пор, пока не сравняется по вертикали с игроком, а затем догоняет его по вертикали. Да, не самый оптимальный и не самый короткий путь. Есть способы лучше.
Уровень сложности
Немного прервём разработку нашего интеллекта и запрограммируем изменение уровня сложности. Самое первое, что приходит в голову - ускорять движение противника в течение игры. Так и сделаем. Помещаем на форму ещё один таймер и называем его LevelTimer; выключаем его, а в качестве интервала задаём то время, через которое уровень должен изменяться. Например, зададим 10 секунд, т.е. изменим Interval на 10000. Кнопка старта игры должна включать и этот таймер: LevelTimer.Enabled:=True; В результате, обработчик нажатия кнопки получается примерно таким:
procedure TForm1.StartButtonClick(Sender: TObject);
begin
Time:=0;
Level:=1;
GameTimer.Interval:=100;
GameTimer.Enabled:=True;
Timer.Enabled:=True;
LevelTimer.Enabled:=True;
StartButton.Enabled:=False;
end;
Ну и наконец, обработчик события OnTimer для LevelTimer:
procedure TForm1.LevelTimerTimer(Sender: TObject);
begin
if GameTimer.Interval >= 15 then
begin
GameTimer.Interval:=GameTimer.Interval-10;
Inc(Level);
label1.Caption:='Уровень: '+IntToStr(Level);
end
else
Игра выиграна
end;
Принцип изменения уровня сложности: изменяем интервал таймера противника - с каждым новым уровнем сложности на 10 мс. Если дошли до маленького интервала, меньше 15 мс, то игру можно считать выигранной. Довольно простая система. Также можно изменять шаг перемещения фигурки противника, делая его больше, но тогда будет создаваться эффект, что фигурка просто перепрыгивает с одного места на другое, а не передвигается. Помимо этого следует отметить, что на компьютерах разной мощности такой способ может работать немного по-разному в плане скорости.
Кстати, чтобы обеспечить более-менее равные условия для всех, имеет смысл жёстко задать размеры формы, иначе владельцы больших мониторов получат огромное пространство для бегства :-) BorderStyle формы устанавливаем в bsSingle, а из множества BorderIcons исключаем biMaximize, чтобы форму нельзя было развернуть. Размеры формы лучше задать в ClientWidth и ClientHeight.
Запустите программу - теперь играть стало сложнее. Наш алгоритм при начальном интервале противника в 100 мс и последовательном понижении его на 10 мс даёт 10 уровней сложности. Дойдёте до конца? Думаю, да. С таким интеллектом далеко не уйти... Действительно, выбирается самый длинный путь, если не считать пути в обход (только этого нам не хватало! ;-) ).
Повышаем уровень интеллекта
Во-первых, неплохо бы слегка оптимизировать наш код - в нём содержится множество обращений к одним и тем же свойствам двух объектов. Лучше завести переменные, значения которых высчитать один раз и впоследствии использовать именно их. Работаем с событием OnTimer объекта GameTimer. Для начала заводим две локальные переменные: Var dx,dy: Integer; Это у нас будут соответственно расстояния между игроком и противником по горизонтали и по вертикали. Для удобства организуем вычисления так, что эти переменные будут принимать как положительные, так и отрицательные значения. Представим 4 координатные четверти плоскости и соответствующим образом расставим знаки наших переменных. Вот вычисление этих расстояний:
if Player.Left+Player.Width < Enemy.Left then
dx:=Player.Left+Player.Width-Enemy.Left
else if Enemy.Left+Enemy.Width < Player.Left then
dx:=Player.Left-Enemy.Left-Enemy.Width
else
dx:=0;
if Player.Top+Player.Height < Enemy.Top then
dy:=Enemy.Top-Player.Top+Player.Height
else if Enemy.Top+Enemy.Height < Player.Top then
dy:=Enemy.Top-Enemy.Height-Player.Top
else
dy:=0;
dx < 0 dy > 0 |
dx > 0 dy > 0 |
|
dx < 0 dy < 0 |
dx > 0 |
Первым делом проверяем, не проиграл ли игрок. Если оба расстояния равны нулю, значит это случилось:
if (dx = 0) and (dy = 0) then
begin
GameTimer.Enabled:=False;
Timer.Enabled:=False;
LevelTimer.Enabled:=False;
GameStatusLabel.Caption:='Вы проиграли!';
Exit;
end;
Ну а если игра всё ещё в процессе, то нужно догонять игрока. Вот тут-то мы и изменим наш алгоритм. Выберем путь короче: будем двигаться не только по горизонтали и вертикали, но и по диагонали. Отдадим приоритет горизонтальному направлению, т.е. сначала будем двигаться по горизонтали и только затем по вертикали и по диагонали. Причина - все экраны вытянуты горизонтально, а значит основной "пробег" будет именно по этому направлению. Итак, если по горизонтали дальше до цели, чем по вертикали, то сначала движемся по горизонтали, а когда расстояния сравняются, пойдём по диагонали под углом 45°. Вот и реализация:
if (Abs(dx) >= Abs(dy)) and (dx <> 0) then
if dx < 0 then
Enemy.Left:=Enemy.Left-Step
else if dx > 0 then
Enemy.Left:=Enemy.Left+Step
else else if (dy <> 0) then
if dy < 0 then
Enemy.Top:=Enemy.Top+Step
else if dy > 0 then
Enemy.Top:=Enemy.Top-Step;
И код короче, и движение эффективнее.
Заключение
Последний алгоритм тоже не оптимален. Это всего лишь один из этапов улучшения интеллекта. Есть ещё более короткие пути и их мы запрограммируем в следующий раз, а также усложним игру новыми способами. Несмотря на то, что данный алгоритм является всего лишь движением одной точки к другой, его тоже можно считать искусственным интеллектом. Он примитивен и очень прост, но он есть и создаёт игровые условия.
Автор: Ерёмин А.А.
Статья добавлена: 11 мая 2007
Следующая статья: Бегущая строка »
Зарегистрируйтесь/авторизируйтесь,
чтобы оценивать статьи.
Статьи, похожие по тематике
- Web-страничка внутри приложения
- Защита формы паролем
- Как изменить иконку у директории?
- Липкие окошки
- Написание инсталлятора на Delphi
- О костылях
- О формах
- Обучающий курс. 15. Подводим промежуточный итог
- Обучающий курс. 4. Свойства в Delphi
- Отображение "пузырей" для значка приложения в System Tray
- Перемещение TImage по форме во время работы приложения
- Пишем компонент - окно выбора папки
- Процедуры для работы с динамическими переменными
- Рефакторинг
- Сохранение настроек
- Управление мышью
Для вставки ссылки на данную статью на другом сайте используйте следующий HTML-код:
Ссылка для форумов (BBCode):
Быстрая вставка ссылки на статью в сообщениях на сайте:
{{a:35}} (буква a — латинская) — только адрес статьи (URL);
{{статья:35}} — полноценная HTML-ссылка на статью (текст ссылки — название статьи).
Поделитесь ссылкой в социальных сетях:
Комментарии читателей к данной статье
Пока нет комментариев к данной статье. Оставьте свой и он будет первым.
Оставлять комментарии к статьям могут только зарегистрированные пользователи.