Простейший 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
dy < 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;  

И код короче, и движение эффективнее.

Заключение

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

Добавлено: 07 Августа 2018 07:55:06 Добавил: Андрей Ковальчук

Ханойская башня

"Ханойская башня" построена на очень простом алгоритме. Здесь я привожу этот алгоритм, который Вы сможете без труда воспроизвести.


type  
  THanoiBin = 0..2;  
  THanoiLevel = 0..9;  
    
  procedure MoveDisc(FromPin, ToPin : THanoiPin; Level : THanoiLevel);  
//  Это Вы должны сделать сами. Переместите один диск с одного штырька на другой.  
//  Диск окажется наверху (естественно, выше него дисков не будет)   

Вы можете каким угодно образом перемещать диски 3-х пирамид. 3 пирамиды - наиболее простая разновидность алгоритма. Таким образом процедура переноса диска (MoveDisc) аналогична операции переноса диска на верхний уровень (MoveTopDisc): переместить диск наверх с одного штырька (FromPin) на другой штырек (ToPin) и передать указатель на штырек-приемник (MoveTower) вместе с уровнем расположения перемещенного диска. Другое решение заключается в использовании трех массивов
 логического типа. В этом случае триггер "Истина (True)" означает наличие на пирамиде диска с размером, соответствующим порядковому номеру элемента массива THanoiLevel.


[DELPHI]procedure MoveTower(FromPin, ToPin : THanoiPin; Level : THanoiLevel);  
begin  
  if HanoiLevel <= High(THanoiLevel) then  
  begin  
    MoveTower(FromPin, 3 - FromPin - ToPin, Level + 1);  
    MoveDisc(FromPin, ToPin, Level);  
    MoveTower(3 - FromPin - ToPin, ToPin, Level + 1);  
  end;  
end;  

Чтобы переместить пирамиду целиком, вы должны вызвать процедуру MoveTower следующим образом:


MoveTower(0, 1, Low(THanoiLevel));  

Добавлено: 30 Июля 2018 19:57:49 Добавил: Андрей Ковальчук

Создание шрифтов формата BMP для использования в играх! Также постейший фильтр изображений

Итак я уже писал про отрисовку таких шрифтов в игре, но в том примере все было сильно неадаптировано под програмный код и занимало несколько страниц!
Для создания своих шрифтов будем писать программу: "ФонтГен"
На форме должны присутствовать:


Button1: TButton; //Кнопка генерации  
Image1: TImage; //Тут будем рисовать  
Edit1: TEdit;//От суда будем брать название шрифта  
Edit3: TEdit;//От суда будем брать цвет шрифта  

На нажатие кнопки(OnClick) пишем:


var  
x,y,z:integer;  
c:char;  
begin  
Image1.Picture.LoadFromFile('shablon.bmp'); //Ну берем 24 разрядный фон из БИТМАПА  
x:=2; //Устанавливаем место отрисовки первой буквы на 2 (первое место будет пробелом)  
for c:='!' to 'я' do //Из таблицы символов от "!" до буквы "я" делаем:  
with image1.Canvas do  
begin  
font.Name:=(edit1.Text); //Узнаем название шрифта  
font.Size:=12; //Его размер ставим на 12  
font.Style:=[fsBold]; //Делаем шрифт ЖИРНЫМ  
font.Color:=stringtocolor(edit3.Text); //Ставим цвет шрифта  
textout(x*20-trunc(textwidth(c)*1.5),10-(textheight(c) div 2),c); //Рисуем данный(по номеру) символ из таблицы символов (см. выше) на этом месте  
x:=x+1; //Следующее место отрисовки  
end; //Отрисовали все символы!!!  
//Далее идет фильтр обводящий каждую букву черным цветом  
for y:=0 to x*20 do //20 - ширина кадра(в одном кадре - одна буква)  
for z:=0 to 20 do  
if image1.Canvas.Pixels[y,z]=stringtocolor(edit3.Text) then //Если цвет пикселя=Цвету шрифта то  
begin  
if image1.Canvas.Pixels[y-1,z]=clWhite then //Если надо то обводим  
image1.Canvas.Pixels[y-1,z]:=clBlack;  
if image1.Canvas.Pixels[y-1,z-1]=clWhite then  
image1.Canvas.Pixels[y-1,z-1]:=clBlack;  
if image1.Canvas.Pixels[y+1,z-1]=clWhite then  
image1.Canvas.Pixels[y+1,z-1]:=clBlack;  
if image1.Canvas.Pixels[y,z+1]=clWhite then  
image1.Canvas.Pixels[y,z+1]:=clBlack;  
if image1.Canvas.Pixels[y,z-1]=clWhite then  
image1.Canvas.Pixels[y,z-1]:=clBlack;  
if image1.Canvas.Pixels[y+1,z+1]=clWhite then  
image1.Canvas.Pixels[y+1,z+1]:=clBlack;  
if image1.Canvas.Pixels[y+1,z]=clWhite then  
image1.Canvas.Pixels[y+1,z]:=clBlack;  
if image1.Canvas.Pixels[y-1,z+1]=clWhite then  
image1.Canvas.Pixels[y-1,z+1]:=clBlack;  
end;  
image1.picture.SaveToFile(edit1.text+'.bmp'); //Сохраняем полученый БИТМАП в "Название Шрифта".bmp  
end;   

Прога действительно делает шрифты, но минус - если менять размер шрифта, то параметры отрисовки каждой буквы придется менять! Иначе возможны глюки!
Теперь идет процедура отрисовки:


Procedure Tmainform.DCN(Text:string;x,y:integer;center:integer);  
var  
h:Tpicturecollectionitem;  
z:integer;  
begin  
h:=imagelist.Items.Find('cnb');  
if center=1 then  
for z:=1 to strlen(pchar(text)) do  
h.Draw(dxdraw.Surface,x+trunc(h.Width*0.65)*(z-1)-trunc(h.Width*0.65)*trunc(strlen(pchar(text))*0.5),  
y,ord(text[z])-32)  
else  
for z:=1 to strlen(pchar(text)) do  
h.Draw(dxdraw.Surface,x+trunc(h.Width*0.65)*(z-1),y,ord(text[z])-32);  
end;   

Как использовать эту процедуру:

DCN({Координаты по икс},{Координаты по игрик},'ТЕКСТ',{Центрировать или нет (1 или 0)}

Вот в принципе и всё.

Добавлено: 30 Июля 2018 19:57:01 Добавил: Андрей Ковальчук

Создание миникарты к стратегиям

Этот пример взят из нашей игры "В ОСАДЕ", способ хорош - быстрый и качественный.
Немного о переменных и числах:
150 - это размер миникарты в пикселах (у нас 150 на 150)
rx и ry - ширина и высота карты (у нас обычно 80 на 80)
Перейдем непосредственно к описанию:


procedure CreatemMap;  
var  
   i,j: Integer;  
   NG: TDIB; //Это картинка в которую мы будем сохранять миникарту  
   Item: TPictureCollectionItem; //Тут будет хранится картинка в TdxImageList'е  
   tm:Tdirectdrawsurface; //Поверхность DxDraw  
   tm2:Tdirectdrawsurface; //Тоже  
begin  
   ng:=tdib.Create; //Создаем компонент  
   tm:=Tdirectdrawsurface.Create(myform.DXDraw.Surface.DDraw); //Создаем поверхность  
   tm.SystemMemory:=false; //Это надо  
   tm.SetSize((150 div rx +1)*rx ,(150 div ry +1)*ry); //Устанавливаем размеры пропорциональные размерам карты  
   tm.Fill(0); //Заоиваем поверхность черным цветом  
   tm2:=Tdirectdrawsurface.Create(myform.DXDraw.Surface.DDraw); //Создаем вторую поверхность  
   tm2.SystemMemory:=false; //Надо  
   tm2.SetSize(150,150); //Устанавливаем сжатые размеры (Настоящий размер миникарты)  
   tm2.Fill(0); //Тоже заливаем черным  
   for i:=0 to rx-1 do //Далее отрисовываем всю карту  
   for j:=0 to ry-1 do //по частям в уменьшеном виде на первую поверхность  
   myform.il.Items.Find('west0').StretchDraw(tm,bounds((150 div rx+1)*i,(150 div ry+1)*j,150 div rx+1,150 div ry+1),map[i,j]);  
   //map - двухмерный массив содержащий номер картинки  
   tm.AssignTo(ng); //Сохраняем содержание поверхности в картинку  
   tm.Free; //Поверхность теперь нам не нужна и мы её удаляем  
   Item := TPictureCollectionItem.Create(MyForm.IL.Items); //Добавляем картинку в TDxImageList  
   Item.Name:='MMG'; //Её имя  
   Item.SystemMemory:=false; //Надо  
   Item.Picture.Graphic := ng; //Загружаем картинку в список  
   item.Transparent:=false; //Непрозрачна  
   Item.Restore; //Принимаем изменения  
   myform.il.Items.Find('mmg').StretchDraw(tm2,bounds(0,0,150,150),0); //Теперь рисуем(заодно и сжимаем) картинку в заданных размерах миникарты на вторую поверхность!  
   tm2.AssignTo(ng); //Опять сохраняем получившийся результат  
   tm2.Free; //Избавляемся от уже ненужной второй поверхности  
   Item.Name:='MMG'; //Исправляем то что нужно в TdxImageList'е  
   Item.SystemMemory:=false; //Исправляем то что нужно в TdxImageList'е  
   Item.Picture.Graphic := ng; //Исправляем то что нужно в TdxImageList'е  
   item.Transparent:=false; //Исправляем то что нужно в TdxImageList'е  
   Item.Restore; //Принимаем изменения  
end; 

Теперь во время игры просто отрисовываем картинку под именем MMG в нужном месте, и если надо то и точки(позиции юнитов на карте) на ней.

Вот в принципе и всё.

Добавлено: 30 Июля 2018 19:56:08 Добавил: Андрей Ковальчук

Система рационального создания спрайтов

B примере мы рассмотрим Четыре способа создания спрайтов:
1. Аркадный
2. Нерациональный
3. Рациональный
4. Рациональный два
Итак способ первый - Аркадный. Его смысл заключается в том, что процедура создания спрайта описывается в коде заранее, а значит параметры спрайта определены ещё до его появления. Вот пример:


constructor TPlayerSprite.Create(AParent: TSprite);  
begin  
   inherited Create(AParent);  
   Image := MyForm.ImageList.Items.Find('PlayerScate');  
   Width := Image.Width;  
   Height := Image.Height;  
   X := 20;  
   Y := 240-Height div 2;  
   Z := 2;  
   PixelCheck:=true;  
   AnimCount := Image.PatternCount;  
   AnimLooped := True;  
   AnimSpeed := 0/1000;  
end;   

Минус сей идеи в том, что каждый раз спрайт будет появлятся в одном и том же месте (в примере по Икс=20 и по Игрик=240 минус половину высоты картинки). Такой способ хорош только для аркад - где главный герой появляется строго в заданном месте.

Второй способ - нерациональный. Каждому спрайту придумать свое имя (не путать с классом). Покажу на примере как это:


procedure TMainForm.FormCreate(Sender: TObject);  
var MASHINA : TplayerSprite; //Вот оно имя спрайта - MASHINA  
begin  
   mashina:=TplayerSprite.create(spriteengine.engine);  
   with TplayerSprite(mashina) do  
   begin  
      Image := MyForm.ImageList.Items.Find('PlayerScate');  
      Width := Image.Width;  
      Height := Image.Height;  
      X := 20;  
      Y := 240-Height div 2;  
      Z := 2;  
      PixelCheck:=true;  
      AnimCount := Image.PatternCount;  
      AnimLooped := True;  
      AnimSpeed := 0/1000;  
   end;  
end;  

Минус этой идеи в том, что спрайтов можно использовать ограниченное количество (только те которые так описаны и имеют свое имя), таким образом чтобы создать 100 спрайтов надо 100 раз написать эту функцию и придумать 100 разных имен!

И вот наконец 3-й - Рациональный способ. Его идея состоит в том, чтобы создать TList добавить в него нужное кол-во спайтов и обработать их в нем.
Вот пример для создания сотни спрайтов класса TPlayerSprite стоящих в ряд:


Procedure TMainForm.StartGameScene;  
var  
   i:integer; //Переменная счета  
   List : Tlist;  
begin  
   List:=Tlist.create;  
   for i:=1 to 100 do  
   begin  
      list.add(TPlayerSprite.create(spriteengine.engine));//Создаем спрайт и добавляем его в TList  
      with TplayerSprite(list.items[0]) do //Устанавливаем его характеристики  
      begin  
         Image := MyForm.ImageList.Items.Find('PlayerScate');  
         Width := Image.Width;  
         Height := Image.Height;  
         X := 20+width*i; //Ставим его на место в ряд по номеру  
         Y := 240-Height div 2;  
         Z := 2;  
         PixelCheck:=true;  
         AnimCount := Image.PatternCount;  
         AnimLooped := True;  
         AnimSpeed := 0/1000;  
      end;  
      list.clear; //Спрайт готов, и в TList он больше не нужен  
   end;  
   list.free; //Спрайты созданы и Tlist нам больше не нужен  
end;   

У этого варианта нет минусов. Именно "мутации" этой идеи используются в большинстве проффесиональных игр.

Совсем забыл вот ещё четвертый:


Type  
   TOrc = class(TImageSprite) //Орк становится подклассом  
   public  
      constructor Create(AParent: TSprite;nx,ny:double);  
   end;  
  
//А вот сама процедура:  
constructor TOrc.Create(AParent: TSprite;nx,ny:double);  
begin  
   inherited Create(AParent);  
   image:=myform.il.Items.Find('stand'); //Ну или свою картинку  
   width:=image.Width;  
   height:=image.Height;  
   x:=nx;  
   y:=ny;  
   //Тут чего нить свое  
end;   

Работает как часы! Орк(в примере) создается командой:
Torc.create(spriteengine.engine,*Ваши координаты*);

Надеюсь то что написано в этой статье вам поможет.

Добавлено: 30 Июля 2018 19:54:33 Добавил: Андрей Ковальчук

Проблема проектирования искусственного интелекта

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

Далее по тексту, "СПР" – сокращение от "система принятия решений". Этим термином будем называть любую систему, которая принимает "ситуацию" на входе и генерирует "ответ" на выходе. Примером СПР может быть простая нейронная сеть, набор жестко заданных правил, набор нечетких логических правил. Наиболее важной особенностью системы должна являться ПРОСТОТА и ЛЕГКОСТЬ МОДИФИКАЦИИ - в частности она должна принимать на входе данные от малого, конечного возможного набора входных данных и генерировать на выходе подобный малый, конечный возможный набор выходных данных.

Некоторое время назад я начал думать о разработки AI для компьютерной игры, требующей, чтобы компьютерный противник избирал разумную военную стратегию. В конечном счете я понял, что СПР с информацией относительно позиции и состояния каждого дружественного и вражеского солдата безнадежно неэффективна - это сродни представлению генерала с такой информацией и ожиданием от его управления движениями каждого солдата!

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

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

Я решил, что эта система может представлять хорошую концептуальную модель для написания искусственного интеллекта в сложной стратегической игре. Дерево СПР может использоваться, для эмуляции цепочки командования в военной иерархии. Определенно, можно использовать относительно более простую СПР для каждого уровня. Входными данными для СПР уровня «k» будут выходные данные уровня «k+1», а выходные, в свою очередь, входными для «k-1» уровня. Выходные данные 0 уровня используются для обновления данных о территории (поле битвы).

Генерал – малое количество параметров позволяют предвидение и рассуждения вида «что-если»
Солдаты – много параметров, но принимаемые решения простейшие, без попыток предвидения и рассуждений вида «что-если»
Основное преимущество этой схемы состоит в том, что она позволяет "более высоким уровням" иерархии формулировать стратегию, без того, чтобы сокрушиться огромным и труднообрабатываемым количеством возможностей, которые компьютерный AI должен был бы рассмотреть, обладай он только информацией об индивидуальных солдатах. Действительно, на самом верхнем уровне, решения будут довольно абстрактные:
- " направлять все военные действия на захват территории X ", или
- " вести войну на истощение на территориях X, Y, и Z ", или
- " выиграть время - придерживаться дипломатии до некоторого времени ", или
- " избегать, прямых столкновений - концентрироваться на прерывании вражеской добычи ресурсов ", и т.д.
При этих обстоятельствах, для компьютера будет возможно рассчитать ситуацию на следующий момент времени (предвидеть). В то время как при обычной системе AI в которой параметры, представлены действиями индивидуальных солдат, это невозможно.

Пример того, как это может быть:
Уровень3 дает прика
-расширение влияния на карте
У2 более конкретизирует этот приказ:
-добыча ресурсов
-атака противника
У1:
-строительство юнитов для добычи ресурсов и строительства базы
-строительство юнитов для атаки
-отдача приказов юнитам
У0:
-передвижение
-атака
-сбор

Предположим:
у0 передвигаясь заметил вражеских юнитов в позиции X,Y передал у1
у1 обработал полученные данные, дополнив их данными о количестве и силе врага и передал параметры у2
у2 просчитал шансы исходя из существующих на данный момент сил и вероятности их увеличения, как со своей, так и с вражеской стороны, и передал данные у3

Тогда:
у3 считает шансы на увеличение влияния на карте высокими и дает приказ на подавление противника у2
у2 выделяет необходимые силы, разрабатывает стратегический план и дает приказ на атаку этой группы противника у1
у1 делит силы на группы, строит группы и дает приказ на атаку каждого отдельного вражеского юнита у0

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

Автор: Andrew Luppnow
Перевел и дополнил: Daddy.

Добавлено: 30 Июля 2018 19:53:39 Добавил: Андрей Ковальчук

Искусственный интеллект в CRPG

Я сейчас работаю над компьютерной RPG. Вот краткий обзор того, что я придумал.

Так как это мой первый опыт в работе над искусственным интеллектом (ИИ) первое, что я сделал, это купил и прочитал книгу "Методы Программирования Искусственного интеллекта: изучение на основе LISP". Из нее я узнал о различных типах искусственного интеллекта и общей идее ИИ. После чтения книги нужно было решить, какой уровень ИИ включить в игру. Нужно ли создавать очень сложный ИИ или просто использовать уловки для придания иллюзии размышления? Сложный ИИ требует слишком много усилий от программиста, и игра не обязательно будет иметь лучший геймплей. А простой ИИ уподобит игру остальным. И еще надо принять во внимание, что я никогда ничего не планирую на бумаге перед написанием программ. В общем я решил сделать гибкий ИИ, достаточно сложный, чтобы быть модифицируемым в игре , вместо того, чтобы модифицировать код и достаточно простым, чтобы быть уверенным в интересности игры.

Теперь, когда уровень сложности ИИ был определен, я принялся к его созданию. Первое что я сделал, это отделил каждый элемент, который составит части ИИ:

Среда - это то, из чего состоит мир. Локациии и объекты. В классическом программировании игр, объекты обычно определяются как переменные и имеют некоторое число параметров.
Например:


Weapon(x).Type = "Меч"  
Weapon(x).Value = 100  
Weapon(x).Damage = 0  

И т.д......

Но этот путь определения объектов потребовал бы изменение кода, если бы я захотел добавить параметр. Так же пришлось бы изменять ИИ, чтобы принять во внимание новый параметр. Так что я решил, что все объекты будут определены "внешне". Объекты имеют некоторые характеристики, и некоторые из них могут изменяться. Например, шар (объект) красный (атрибут цвета) и может быть брошен (действие) и если, это случится - местоположение (атрибут), изменится. С этими элементами я могу начать написание редактора. Я сделал " редактор атрибутов ", "редактор объектов" и " редактор действий ".

Редактор атрибутов позволяет создавать атрибуты типа "состояние открытости" для двери или любого другого объекта, который может быть открыт или закрыт. Этот атрибут может иметь значение "открыт" или "закрыт". Атрибут "повреждения" - может иметь значение от 0 до 100 и отражает степень повреждения объекта. Это позволяет ИИ проверять атрибут объекта и пробовать при необходимости изменить его. Например, персонаж может "видеть", что дверь закрыта и если нужно, может пробовать открыть ее.

Теперь, когда атрибуты созданы, нужен способ изменения их игроком или персонажем. Для этого используется "редактор действий". Действие состоит из нескольких ключевых элементов. Сначала мы должны убедиться, что действие доступно только тогда, когда определенные условия истинны. Например, действие "открыть" не должно быть доступно на двери, которая уже открыта. Для этой цели используются "необходимые атрибуты". Раздел "необходимых атрибутов" содержит все атрибуты, которые проверяются, для определения доступности действия. Так при действии "открыть" проверяется атрибут "состояние открытости" на значение "закрыто". После того, как все требуемые атрибуты проверены, можно переходить к разделу "проверки атрибута". Здесь, если атрибуты имеют нежелательное значение, игрок уведомляется об этом. Например, если атрибут двери "состояние блокированности" в значении "заперто", игра может сообщить "эта дверь заперта, отоприте сначала" . И наконец, есть раздел "изменения атрибута". В примере двери этот раздел содержал бы изменение атрибута "состояние открытости" до значения "открыто".

Редактор объекта позволяет давать различным объектам различные атрибуты и возможные действия. Например, после создания объекта "дверь", мы можем назначать ему атрибут "состояние открытости", чтобы знать, является ли дверь открытой или закрытой. Потом мы можем назначать, атрибут "состояние блокированности", чтобы знать, блокирована ли дверь. Теперь мы можем создать действия "открыть" и "закрыть" и присвоить их двери. Как только создание объекта дверь закончено, мы можем создавать объект "ключ" и назначить ему действия "запереть" и "отпереть", которые могут изменять атрибут "состояние блокированности".

Наконец, самое интересное, как ИИ будет работать с этими данными.

ИИ теперь имеет достаточно данных, чтобы размышлять. Эта схема может работать с любыми созданными объектами, что не требует писать определенный код для каждого различного объекта. Вот пример того, как это работает.

Продолжая пример с дверью: персонаж может "видеть", что атрибут "состояние открытости" двери имеет значение "закрыто". Если по какой либо причине, персонаж "хочет", чтобы значение было "открыто", он "думает" по следующему пути. Сначала он проверяет, какие действия изменяют атрибут "состояние открытости" объекта. И находит, что это делает действие "открыть". Теперь, когда персонаж знает, какое действие нужно сделать он проверят, какие атрибуты являются необходимыми этого для действия. Действие "открыть" требует, чтобы атрибут "состояние открытости" объекта, был равен значению "закрыто". Затем он проверяет "проверки атрибутов". Предположим, что атрибут "состояние блокированости" имеет значение "заперто". Теперь, "зная", что нужно изменить значение "состояние блокированности" двери, ИИ продолжает исследовать, и находит, что изменить состояние можно действием "отпереть". После выполнения действия "отпереть" на двери персонаж может выполнить действие "открыть" и наконец получить открытую дверь. Как вам это? Теперь у нас есть ИИ, который может взаимодействовать с поддающимся изменению миром.

На этом месте я остановился. Далее, вероятно следует идти в этих направления:

Персонажи должны иметь собственную базу знаний, это оградит их от "знания всего". Например, в разделе проверок действия ключа "отпереть", может быть атрибут "уникальный идентификатор". Этот атрибут указывает, что ключ разблокирует только "ту-самую" дверь (key.uniqueID=door.uniqueID). Но персонаж не должен знать, какая именно дверь имеет "тот-самый" идентификатор. То-есть должен быть своего рода "фильтр", который скрывал бы двери и идентификаторы ключей, пока не будут найдены правильные комбинации ключ-дверь.
Персонажи должны иметь побуждения или цели. Они могут взаимодействовать с миром, но им нужны причины, для того чтобы что-то делать. Это можно достичь в соответствии с требованиями атрибутов. Например, атрибут "состояние сытости" персонажа должен всегда быть ниже 90 % (character.hungry< 90) что бы заставить персонаж иногда есть. Этот атрибут повышается с течением времени, и "применение" еды приводит к его понижению.

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

Вот в принципе и все. Надейтесь, что вам понравилось.

Добавлено: 30 Июля 2018 19:53:08 Добавил: Андрей Ковальчук

ИИ с чего начать ?

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

Непрограммируемый: на примере игры MindRover

MindRover - игра, произведенная крмпанией CogniToy, в которой игрок выбирает вездеход, добавляет компоненты и соединяет их с помощью проводов, для выполнения некоторой задачи: чего-нибудь взорвать, выиграть гонку, управлять схемой или все сразу!
MindRover это превосходная платформа для экспериментов с игровым искусственным интеллектом, потому что она не требует умения программирования. Все «программирование» сделано через графический интерфейс (на рисунке). Выбираем транспорт, добавляем радар, управляющие компоненты и оружие. После, соединяем компоненты так, чтобы при обнаружении чего-либо радаром, транспорт стрелял по цели и двигался к ней. Итак запускаем сценарий и находим, что вездеход в порядке, но если противник выстреливает медленно летящую ракету, радар обнаруживает ее и поворачивается к ней. Это не совсем то, что нам надо, поэтому возвращаемся назад и добавляем IFF (опознаватель "свой-чужой") фильтр на радаре, и отфильтровываем все, что не является врагом. Запустим сценарий, теперь вездеход работает намного лучше.
В MindRover роботы могут быть гораздо более сложными чем этот и содержать много компонентов (до 30). Они могут быть связаны через логические выходы, радиопередачи (один элемент включает несколько), переключатели (разрешают и запрещают события), и тд.
MindRover основан и является графической оболочкой для языка программирования ICE. Ниже - кусок кода, который сгенерирован схемой вездехода, показанного в вышеупомянутом скриншоте.


class DkrPursuit extends Vehicle  
sys as VehicleSystemObject  
  
Filter_IFF1 as Filter_IFF  
MediumEngine1 as MediumEngine  
Steering1 as Steering  
BumpSensor1 as BumpSensor  
Filter_IFF2 as Filter_IFF  
LongRangeRadar1 as LongRangeRadar  
MediumRadar1 as MediumRadar  
MediumRadar2 as MediumRadar  
MediumRadar3 as MediumRadar  
MediumRadar4 as MediumRadar  
Broadcast1 as Broadcast  
TurnLeft as Broadcast  
Timer1 as Timer  
  
' class functions are here...  
  
end class 'DkrPursuit  

В этой статье мы не будем касаться программирования на ICE, его возможности гораздо шире чем в графическом интерфейсе и описывать их очень долго.
В настоящее время есть группа программистов на ICE, которые расширяют набор компонентов в игре, типа более продвинутых логических выходов, компоненты, которые эмулируют память СППЗУ и более другие вещи. Так что, если вы еще не знаете высокоуровневый язык программирования, обратите внимание на MindRover. По мере того, как вы будете входить в суть, сможете программировать дополнительные компоненты и более интеллектуальные вездеходы.

Создание модов. Программируемый

С появлением игр типа Quake II, разработчики обратились к созданию легко расширяемых игр. Весь код Quake II был в DLL, которые грузились во время загрузки игры. ID Software (разработчики Quake) выложили исходники, позволив любому изменить оружие, аспекты процесса игры, физику и, что наиболее важно (для нас), искусственный интеллект.

Изменять ИИ для игр такого типа относительно легко, начиная с изменения значения параметров, изменения функциональных возможностей ИИ и заканчивая внедрением новых особенностей ИИ.

На скриншоте один из результатов моего Quake II mod, который улучшен в плане искусственного интеллекта, весьма значительно. На нем вы видите солдата, подпрыгнувшего над выстрелом, нацеленного ему в ноги, и стреляющего при этом. Этот мод имеет следующие параметры:
• Уворачивание. Враг уворачивается, только когда необходимо.
• Некоторые враги подпрыгивают, когда выстрелы направлены им в ноги.
• Монстры могут плавать. По умолчанию монстры в Q2 не плавают,
• монстры не будет стрелять, когда один из них – мешает обстрелу.
• Экспериментировал со взрывчаткой.
• Экспериментировал с алгоритмами поиска пути.

Уворачивание просто требовало подбора числа. В конечном счете я научился вычислять время, требуемое для уворачивания от выстрела. Что означает, что стало очень трудно уничтожить кого-нибудь из бластера.
Функция прыжка появилась несколько позже, когда я стал более свободно разбираться в исходных текстах, так как нужно было изменить игровые структуры. Я использовал мультипликацию от уворачивания, но запустил врагов в воздух, чтобы прыжок был более реалистичным! Так как враги стали после этого увертываться, прыгать и отступать после атаки игрока, первый уровень стал очень труднопроходимым, без дополнительного оружия.
Плавание также требовало много модификаций, так как большинство монстров не умеют плавать. Так, я нашел, что монстры после попадания в воду не двигались, пока они не поражали пол. В общем я добавил специальный флажок, что разрешило им плавать и ходить по земле!
Изменяя ИИ вы можете получить абсолютно непохожую на оригинал игру. Другие игры типа Unreal, HalfLife и Quake III также позволяют изменять код. Так что, если у вас есть, какая-нибудь из этих игр и вы знаете C/C++ - модифицируйте их на здоровье! (ну а мы как-нибудь на Delphi перебьемся ;) )

С нуля...

Настольные игры - превосходный выбор для написания ИИ, так как они имеют очень простую графику (которая может многократно использоваться в других играх) и правила - необходимую часть ИИ. Вот некоторые из настольных игр, с указанием уровня сложности реализации ИИ:
• Pente (средний)
• Крестики-Нолики (легкий)
• Othello (легче среднего)
• Шашки (средний)
• Шахматы (тяжелый)
• Connect 4 (легкий)
• Go (тяжелый)
• 3D Крестики-Нолики (легче среднего)

Написание настольных игр - наилучший путь к изучению принципов искусственного интеллекта, так как довольно сильные противники могут быть созданы, через использование простой эвристики (см. Простой ИИ Настольной игры) без использования сложных методов. Можно расширить ИИ игры "игровыми деревьями" и "минимаксным" алгоритмом. Выгодное отличие в создании игры с нуля это, то что вы сами решаете насколько простым или сложным будет алгоритм, и вы можете постоянно менять его. Конечно, написание любой игры может утомить, поэтому не ставьте перед собой заоблачные планы. Будет гораздо интереснее добавлять и расширять возможности работающей игры, чем ковыряние в супер алгоритме незаконченной пре-альфы.

Добавлено: 30 Июля 2018 19:52:34 Добавил: Андрей Ковальчук

Изменение гаммы спрайта

Для чего это нужно. Эту технологию можно использовать для выделения "юнита", или для выделения пункта меню. Одним словом для обозначения чего-либо.
Ну что не поняли?

Как это делатся. Динамически создается новый спрайт с подложкой нужного цвета (фон) и на него накладывается с использованием Альфа смешения наш спрайт. Чем больше величина Альфа, тем больше будет "проявляться" фон, соответственно, если "фон" белый - спрайт будет осветляться.
Что касается TransparentColor - если фоновый цвет спрайта будет отличаться от "подложки" создаваемого спрайта, то придется подумать как определить новый TransparentColor. Можно конечно заранее его просчитать и использовать, а можно просчитать динамически, взяв, например, цвет фоновой точки (посмотрите координаты какой нибудь из них в редакторе) в полученном спрайте.

Посмотрим на примере.
Эта процедурка реализует алгоритм описанный выше.


procedure Tform1.createmmap(Color:Tcolor;alpha:byte);  
var  
   NG : TDIB;  
   Item: TPictureCollectionItem;  
   tm:Tdirectdrawsurface;  
   i:integer;  
   est:boolean; //Присутствует ли уже измененная картика в списке  
   TC:TColor; //TransparentColor  
begin  
   ng:=tdib.Create; //Создаем картинку (пустую)  
   tm:=Tdirectdrawsurface.Create(DXDraw.Surface.DDraw); //Создаем поверхность  
   tm.SystemMemory:=false;  
   tm.SetSize(il.Items[0].Width,il.Items[0].height); //Устанавливаем размер поверхности по размеру исходной картинки  
   tm.Fill(color); //Заполняем фон нужным цветом  
   il.Items[0].DrawAlpha(tm,bounds(0,0,il.Items[0].width,il.Items[0].height),0,alpha); //Рисуем исходную картинку на фон с прозрачностью  
   tm.AssignTo(ng); //Сохраняем изображение в картинку (создана первой строкой)  
   tm.Free; //Очищаем память от поверхности  
   TC:=ng.Canvas.Pixels[0,0]; //Возьмем цвет точки 0,0 в качестве TransparentColor для нового спрайта  
   est:=false; //Ставим на нет, то что картинка уже есть в списке  
   for i:=0 to il.Items.Count-1 do //Если же она есть  
   if il.Items[i].Name='MMG' then est:=true; //ТО Ставим на Да  
   if est then //Если ДА то  
   begin //Изменяем уже существующую картинку  
      item:=il.Items.Find('MMG');  
      item.Picture.Graphic := ng;  
      item.TransparentColor:=TC;  
      item.Transparent:=true;  
      item.Restore;  
   end  
   else  
   begin //Если же Нет, то заносим в список новую!  
      Item := TPictureCollectionItem.Create(IL.Items);  
      Item.Name:='MMG';  
      Item.SystemMemory:=false;  
      Item.Picture.Graphic := ng;  
      Item.TransparentColor:=TC;  
      Item.Transparent:=true;  
      Item.Restore;  
   end;  
end;  

Добавлено: 30 Июля 2018 19:51:06 Добавил: Андрей Ковальчук

DXInput

В этой статье мы рассмотрим методы управления объектами в играх с помощью клавиатуры, мыши и джойстика..

1. Клавиатура.
Опрос нажатия клавиши ведется через DXInput.States:


if <тикер> in DXInput.States then  
begin  
   //действие  
end  

,где <тикер> есть зарезервированное слово:

isUp - по умолчанию "стрелка вверх"
isDown - по умолчанию "стрелка вниз"
isRight - по умолчанию "стрелка вправо"
isLeft - по умолчанию "стрелка влево"
IsButton1 - по умолчанию "Space"
IsButton2 - по умолчанию "Enter"
IsButton3-IsButton32 - по умолчанию не определены

Для сравнения два равноценных условия:


//c использования DXInput  
if isUp in DXInput.States then  
begin  
//действие  
end  
  
//-------  
  
//а это - без  
if Key=VK_UP then  
begin  
//действие  
end  

Чтобы определить "тикер":

DXInput.Keyboard.KeyAssigns[<тикер>,X]:= <кнопка>

,где:
X - значение от 0 до 2 (то есть на каждый "тикер" можно присвоить до трех <кнопок> )
<кнопка> - код кнопки (например ord('Q')).
Примечание: буквенные клавиши указываются в верхнем регистре,
т.е. DXInput.Keyboard.KeyAssigns[isButton10,X]:=ord('q') не будет реагировать на нажатие клавиши 'q'

Кнопки Ctrl, Alt, Shift можно опросить обычным способом:


If (ssCtrl in Shift) then ...  
If (ssAlt in Shift) then ...  
If (ssShift in Shift) then ...  

Небольшой примерчик. Зарекрепим за тикером isUp - кнопки "E","I" и "стрелка вверх":


procedure TForm1.DXTimerTimer(Sender: TObject; LagCount: Integer);  
begin  
   if not DXDraw.CanDraw then exit;  
   //обновим состояние DXInput (нужно делать при каждом тике таймера)  
   DXInput.Update;  
   DXDraw.Surface.Fill(0);  
   If isUP in DXInput.States then  
      With DXDraw.Surface.Canvas do  
      begin  
         Brush.Style := bsClear;  
         Font.Color := clWhite;  
         Font.Size := 12;  
         TextOut(50,50,'UP pressed');  
         Release;  
      end;  
   DXDraw.Flip;  
end;  
  
procedure TForm1.FormCreate(Sender: TObject);  
begin  
   DXInput.Keyboard.KeyAssigns[isUp,0]:=ord('E');  
   //за [isUp,1] - по умолчанию зарезервирована "стрелка вверх", оставим ее.  
   DXInput.Keyboard.KeyAssigns[isUp,2]:=ord('I');  
end;   

2. Мышь

DXinput.Mouse.X - положение курсора по горизонтали
DXinput.Mouse.Y - положение курсора по вертикали
DXinput.Mouse.Z - положение курсора по третьей оси

Небольшой примерчик:


var  
   Form1: TForm1;  
   MouseX,MouseY:integer;  
   LeftB,RightB:boolean;  
  
implementation  
  
{$R *.DFM}  
  
procedure TForm1.DXTimerTimer(Sender: TObject; LagCount: Integer);  
var Shift: TShiftState;  
begin  
   if not DXDraw.CanDraw then exit;  
   DXInput.Update;  
   DXDraw.Surface.Fill(0);  
   With DXDraw.Surface.Canvas do  
   begin  
      Brush.Style := bsClear;  
      Font.Color := clWhite;  
      Font.Size := 12;  
      If LeftB then  
         TextOut(0,0,'Left');  
      If RightB then  
         TextOut(50,0,'Right');  
      TextOut(0,50,IntToStr(MouseX)+','+IntToStr(MouseY));  
      Release;  
   end;  
   DXDraw.Flip;  
end;  
  
procedure TForm1.DXDrawMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);  
begin  
   MouseX:=X;  
   MouseY:=Y;  
end;  
  
procedure TForm1.DXDrawMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);  
begin  
   If ssLeft in Shift then LeftB:=true;  
   If ssRight in Shift then RightB:=true;  
end;  
  
procedure TForm1.DXDrawMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);  
begin  
   LeftB:=false; RightB:=false;  
end;   

3. Джойстик

Настроим джойстик:

DXinput.Joystick.RangeX = 0 - 1000, устанавливает диапазон изменения значений по горизонтальной оси от X до -X
DXinput.Joystick.RangeY = 0 - 1000, устанавливает диапазон изменения значений по вертикальной оси от Y до -Y
DXinput.Joystick.RangeZ = 0 - 1000, устанавливает диапазон изменения значений по третьей оси от Z до -Z

DXinput.Joystick.DeadzoneX = 0 - 100, устанавливает чувствительность по горизонтальной оси
DXinput.Joystick.DeadzoneY = 0 - 100, устанавливает чувствительность по вертикальной оси
DXinput.Joystick.DeadzoneZ = 0 - 100, устанавливает чувствительность по третьей оси
0 - самое чувствительное значение.

Читаем положение рукоятки:

DXinput.Joystick.X - по горизонтальной оси
DXinput.Joystick.Y - по вертикальной оси
DXinput.Joystick.Z - по третьей оси

Z - используется в случае наличия на джойстике соответствующей ручки.
Этот кусок кода сканирует кнопки на джойстике:


for i := 0 to DXInput1.Joystick.ButtonCount do  
begin  
   if DXInput1.Joystick.Buttons[i] = True then  
   begin  
      //Кнопка нажата. Действие.  
   end;  
end;  

Примечание: Руль можно считать частным случаем джойстика. При этом штурвал - горизонтальная ось (X), педаль газа и педаль тормоза - вертикальная ось (Y).

Добавлено: 27 Июля 2018 22:48:55 Добавил: Андрей Ковальчук

Advanced Draw

Кручу-верчу, обмануть хочу, или как использовать продвинутые методы вывода спрайтов.

В классе TSprite есть три метода:
DoDraw, DoCollision и DoMove
Чтобы заставить спрайт созданный таким образом:

TPlayer = class(TImageSprite)  
end;

выводится через продвинутые методы (DrawRotate, DrawAplha, DrawSub, DrawWave, StretchDraw), нужно "заглушить" стандартный вывод при создании объекта. Вот так:
type  
   THero = class(TImageSprite)  
      Angle:integer;  
   protected  
      procedure DoCollision(Sprite: TSprite; var Done: Boolean); override;  
      procedure DoMove(MoveCount: Integer); override;  
      procedure DoDraw; override; // вот здесь  
   end;

Теперь в процедуре DoDraw выводим спрайт нужным нам способом. Вот так:
procedure THero.DoDraw;  
begin  
   image.drawrotate(form1.DXDraw.Surface,round(x)+16,round(y)+16,image.width,image.height,round(animpos),0.5,0.5,Angle);  
end;

А теперь недокументированная фишка!
Забываем то, что написано выше и читаем дальше.
Cоздадим спрайт таким образом:
TPlayer = class(TImageSpriteEx)  
protected  
   procedure DoMove(MoveCount: Integer); override;  
end;

И теперь в процедуре DoMove можно задать Angle (угол поворота спрайта) и Alpha (прозрачность спрайта). Примерно так:
type  
   THero = class(TImageSpriteEx)  
      Angle:integer;  
   protected  
      procedure DoCollision(Sprite: TSprite; var Done: Boolean); override;  
      procedure DoMove(MoveCount: Integer); override;  
   end;  
  
procedure TPlayer.DoMove(MoveCount: Integer);  
begin  
   X:=100;  
   Y:=100;  
   Angle:=60;  
   Alpha:=150;  
end;

Дерзайте!

Добавлено: 13 Июля 2018 08:18:33 Добавил: Андрей Ковальчук