Как написать игру на delphi

Всем привет! Я хочу рассказать историю разработки игры на Delphi от идеи до релиза.
Статья больше носит характер истории, без вникания в подробности реализации. Честно говоря, очень хочется написать про то, как мы строим картинку в игре, или локализуем её, как мы делали GUI, но это я выделю в отдельную статью, больше техническую, с кодом. Иначе эта окажется слишком большой. Кода тут не будет, но детали под катом. Прошу!

Application.Initialize;

Итак. Игра наша придумана была давно для конкурса разработки игр (джема) IGDC №77 — джампер Это было начало 2012 года. Игру мы делали в состоянии перманентного кранча аж две недели. Код вышел ужасным. Расширять было невозможно, порой возникали необъяснимые глюки, но в целом игра работала и некоторые товарищи умудрялись проводить в ней дни, недели. Пара человек залипали в течение месяца. Стало понятно, что реиграбельность у неё хорошая и надо бы её доделывать. Но, как я сказал выше, код был ужасен из-за скомканных сроков, так что доделок было сделано не очень много и всё благополучно забыто.

Выглядело это тогда так:

Для сравнения сейчас это выглядит так:

While true do

Забыто было почти до 2014 года. Тогда мы собрались и начали делать всё снова. С капитальным подходом к архитектуре. Были заложены такие вещи как менеджер игровых режимов, система динамической локализации, поддержка любых разрешений, достижения и прочее, прочее, прочее… Всё на классах, всё унаследовано, перекрыто. Что не в классах, а глобально, то в статических классах. Вообщем подошли к вопросу очень отвественно. Про игру, правда, около года благополучно забывали. Было там что-то, отдаленно напоминающее давний прототип, но до него не дотягивало ни по контенту, ни по интересности. Прокрастинировали активно и долго, короче. Вот меню именно тогда собрали, я сделал планеты и даже статью по ним на Хабре.

Польза от такой прокрастинации, тем не менее, была. Позже все эти вещи помогли избежать вороха проблем, поскольку были реализованы в фундаменте. Локализация, например, была решена посредством XML файлов, прямо перед релизом к нам постучался итальянец, Франческо и предложил перевести игру. Сделал всё сам, мы нарисовали нужные символы в шрифте и игра за неделю обзавелась итальянским. Легко и просто. Из этого как минимум один вывод сделать стоит — не забивайте на локализацию, сделайте её пораньше. И желательно не в ущерб геймплею.

For feature in coolthings do

Когда мы начали делать игру, мы смотрели, на лучшие моменты в разных играх, но больше всего оглядывались на Blizzard. О да, это очень крутая контора, они делают крутые игры и каждая хит. А еще каждая проработана до мелочей. И это нас безумно цепляло. Поэтому я, как поклонник Diablo и мой коллега как поклонник WarCraft и StarCraft смотрели на них часто. Так было решено что, например, достижения или друзей надо иметь возможность просматривать прямо из игры, без оверлеев или выхода. Так и появились такие мелочи, как часы в углу, как затухающая музыка при alt+tab, возможность прослушать любой трек из меню, в любой момент подключить геймпад и играть не перезапуская, показывать достижения прямо в игре и многое другое.

Игру мы делали почти всё время вдвоем. Два программиста. Графику или рисовал сам, или генерировал спрайты с помощью FilterForge Под конец разработки мы заказали графику для врагов, купили иконки в ассетсторе, звуки. Музыку нам делали на заказ и музыка появилась еще год назад. Музыку не просто написали нам, так еще и сводили на проф. оборудовании в студии. Треки получились отличные, и очень в настроение игры.

Case build of release

Код. Код мы пишем на Delphi 10.1 на текущий момент, а движок используем свой Quad-engine, тоже написанный на делфи с открытым исходным кодом. Разработка игры, кстати, безумно помогает сделать движок действительно полезным и удобным. Мы давно работаем в делфи и нам удобен этот инструмент, но даже тут не обошлось без косяков. В одном из модулей GUI сборка под релиз и дебаг работала по разному. Внезапно оказалось, что пункт «оптимизация» в релизной версии почему-то делает одну из переменных True в случаях, когда она должна быть False. И GUI начинает вести себя неадекватно. Ассемблерный код разный и результат тоже. Решение выбрано не самое лучшее было, но мы отключили пункт «оптимизация» воимя одинакового с дебагом результата.

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

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

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

Game.Draw;

Нормалмаппинг, объемное освещение, искажения пространства, параллакс маппинг и куча более мелких, но не менее важных шейдеров. Шейдерами делается практически всё. От цветокоррекции до переливания щитов. Многие эффекты вы можете и не заметить, или подумать что они нарисованы заранее, но нет. Они делаются «на лету».

Вот, например, скриншот без цветокоррекции:

function IsThereTroubles: Boolean; abstract;

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

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

Для подогрева интереса к игре, чтобы игрок не разочаровался в себе и не бросил сразу, если дела идут не очень круто, мы добавили в игру соревновательную составляющую. Определенные виды статистики сохраняются и друзья могут сравнить свои результаты с вами, а вы с друзьями. Кроме того опыт и открывание новых умений с каждым «левелапом» тоже помогает посидеть в игре чуточку дольше.

Target platforms (Win32)

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

Другой интересной проблемой стало падение Steamworks API при попытке получения информации о друзьях. На наших аккаунтах с 40-50 друзей все было «ОК», но вот у стримеров и летсплееров, где друзей было по 500+ оно где-то посередине списка падало по необъяснимым причинам. Так мы чуть не упустили еще пару стримеров, но поправить удалось очень быстро. В тот же день был сделан хотфикс.

Дальнейшая судьба игры — доработка, расширение. Крайне с удовольствием слушаем любую обратную связь, предпринимаем действия, помогающие сделать всё еще лучше. Сделаем Делфи снова великим!

Я добавил небольшой опрос в конце, буду благодарен за честный ответ.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

О чем из нашей игры было бы интересно почитать подробно в следующей статье?


53.38%
Графикашейдеры
71


26.32%
GUI и локализация
35


54.14%
Игровой цикл, логика геймплея
72


32.33%
Пишите что-нибудь, всё сойдёт
43


5.26%
Не надо больше ни о чем писать
7

Проголосовали 133 пользователя.

Воздержались 19 пользователей.

Вэтой статье я подробно рассмотрю создание игры «Змейка» с использованием ООП (объектно-ориентированного программирования). Делать игру мы будем средствами GDI на Delphi 7 (хотя должна подойти и любая другая версия). Для полного осознания того, о чем говорится в статье желательно иметь хоть какой-нибудь опыт программирования, знать что такое Сanvas и TBitmap, что такое классы и в чем заключаются их особенности.

Вадим Буренков

Вообще, при создании любой игры можно выделить три момента:

  1. Создание идеи, примерный план реализации. При желании разработчиком пишется «диздок» (дизайн-документ — это документ, который описывает то, что вы хотите создать).
  2. Алгоритмизация. То есть мы представляем план строения игры, какие типы данных будут использоваться. Обдумываются алгоритмы работы и взаимодействия элементов игры.
  3. Все выше мы записываем в виде кода, то есть программируем.

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

Краткий экскурс…

При создании любой игры нужно придерживаться определенного строения кода.
Каждая игра имеет следующие секции:

  • инициализация (тут происходит настройка окна для рендера (вывода изображения), загрузка всех игровых ресурсов и уровня)
  • обработка (всех игровых объектов, их взаимодействие друг с другом. Эта секция в отличие от инициализации и деинициализации выполняется постоянно, причем скорость выполнения измеряется в миллисекундах и обычно** равна 10-100 мс)
  • деинициализация (выполняется при завершении работы приложения и служит для очищения занятой приложением памяти)

Обратите внимание! Компонент TTimer имеет низкую точность. Ставить на нем низкие значения (менее 30 мс) не имеет смысла.

Сделаем основу для игры

Откройте Delphi, создайте новое приложение. Сразу хочу заметить, что именам всех переменных, окон, файлов, и.т.п. надо давать осмысленные имена. Конечно, кто-то скажет что и так запомнит что такое Form1 и Label2, но когда пишется проект не на 10 а на 10000 строк и используется не одна, а пятнадцать форм… Также не надо мелочиться на комментарии, поскольку смысл определенного кода через месяц простоя не поймет и тот, кто его написал, не говоря уже о том – что вам может понадобиться сторонняя помощь.

Назовем форму MainForm, модуль GameMain, а проект SnakeProject. Поскольку эта игра простая и небольшая, то я буду использовать один модуль, но в остальных случаях рекомендуется использовать свой модуль для отдельной игровой части. Создадим два события формы: OnCreate и OnDestroy которые и будут инициализатором и деинициализатором игры. За обработку игры будет отвечать компонент-таймер TTimer (поместите его на форму из вкладки System, назовем его MainTimer), а именно его событие – OnTimer.

Я не буду делать игру очень гибкой в плане игрового поля (его размеры нельзя будет менять. Поле будет 16х12 клеток, но о них будет написано ниже, основные параметры будут константами. Игровое же поле 640х480 по центру экрана без возможности перемещения (без синей рамки). Объявим константы:

Const // разрешение игрового поля
SCR_WIDTH=640;
SCR_HEIGHT=480;

Можно не выставлять параметры окна в среде разработки, а написать их в коде (событие OnCreate):

// установка размеров окна
MainForm.Width := SCR_WIDTH;
MainForm.Height:= SCR_HEIGHT;
// параметры формы
MainForm.BorderStyle:= bsNone; // без рамки
MainForm.Position := poDesktopCenter; // по центру экрана

Теперь заставим работать таймер (изначально параметр таймера должен быть enable = false):

MainTimer.Interval := 40;
MainTimer.Enabled:= true;

Создадим и добавим в событие OnTimer две процедуры (пока пустые) – Main_Update() и Main_Draw(). Для чего это? Мы ведь можем писать весь код напрямую в таймер. Поясню. Например, если мы захотим сделать в игре меню, то должны быть отдельные процедуры обработки/рисования для меню и игры и выполняться они будут в зависимости от того, где игрок находится:

Scene: (sMenu, sGame);
if Scene = sMenu then Menu_Draw;
if Scene = sGame then Main_Draw;

Отрисовка

В этой статье я не буду делать меню, но все же. Итак, мы получили некую заготовку игры. Теперь надо правильно настроить рендер. Canvas отличается простотой, но его недостаток – медленная скорость. Если просто выводить спрайты (изображения) на форму, то будет видно – что они выводятся не сразу, а один за другим и появляется эффект мерцания изображения. Чтобы избежать этого – нужно выводить спрайты не на экран, а в буфер памяти. После того как финальное изображение будет построено, можно вывести его на форму. После вывода буфер можно очистить и он будет готов для построения следующего кадра.

Теперь реализуем данную систему. Объявим переменную – буфер scr_Buffer как Tbitmap. Перед использованием, буфер нужно проинициализировать и установить размеры:

// инициализация буфера
scr_Buffer:= TBitmap.Create;
scr_Buffer.Width := SCR_WIDTH;
scr_buffer.Height:= SCR_HEIGHT;

И в событии Main_Draw() напишем код отрисовки буфера (все рисование должно происходить до этих строчек):

MainForm.Canvas.Draw(0, 0, scr_Buffer); // копируем содержимое буфера на экран
scr_Buffer.Canvas.Rectangle(0, 0, SCR_WIDTH, SCR_HEIGHT); // очищаем буфер ***

Тут же можем проверить, как наша заготовка справляется с рисованием. Загрузим фон для игры в переменную BackImage:

// создание
BackImage:= Tbitmap.Create;
BackImage.LoadFromFile(‘fon.bmp’);
// отрисовка
scr_Buffer.Canvas.Draw(0 ,0, BackImage);
// очищение
BackImage.Free;

Нажимаем клавишу <F9> и любуемся на результат (см. рис.1):

Рис. 1. Вывод фоновой картинки игры

Концепция игры. Долгосрочная стратегия

Итак, мы сделали «скелет» игры, на который будем насаживать игровой код. Обдумаем, что же будет представлять из себя игра? Есть змейка, она ползет по уровню, маневрирует между препятствиями и ест все съедобное на ее пути. Но змея не должна есть стены и себя, иначе она погибнет. Из выше написанного подобия «диздока» можно выделить два класса, которые мы будем использовать в игре: змея (назовем ее TSnake) и остальные объекты (TGameObject). Объекты будут иметь переменную, которая определяет их тип: либо это еда, либо препятствие. Каждый объект имеет координату. Поле будет поделено на клетки, это позволит легко определять столкновения (если координаты двух объектов совпадают, то они столкнулись). Размер каждой клетки будет определяться константой CAGE_SIZE = 40. Зная координату клетки можно получить координату на экране, умножив ее на размер клетки. Это нам понадобится при рисовании****. Эти вспомогательные функции делают следующие:

Function G_X(x:integer):integer;
begin
 result:= x*CAGE_SIZE
end;
Function G_Y(Y:integer):integer;
begin
 result:= Y*CAGE_SIZE
end;

Программируем поведение змейки

Обдумаем, как же будет работать змейка. Каждая деталь змейки – это отдельный элемент со своими координатами. Когда змейка движется – первый элемент (голова) занимает новую позицию, а второй элемент занимает позицию предыдущего до перемещения (там, где раньше была голова), третий позицию второго и.т.д. Это самый логичный вариант, который приходит в голову. Но тогда надо сохранять координаты, где было тело змейки. Для упрощения кода есть более легкий способ – двигать змейку с хвоста. Перейдем к созданию класса змейки:

type
TSnake=class
 Sprite:TBitmap; // спрайт составной части змейки
 len:integer; // длина змейки (из какого кол-ва частей состоит змейка)
 BodyPos: array of vect2D; // динамический (!) массив с координатами частей змейки
 Speed:vect2D; // скорость змейки (сколько клеток она проходит за ход)
 MoveTo:(left,right,up,down); // показатель направления движения
Constructor Create(spr_name:string;pos:vect2D;size:integer); // создание змейки со спрайтом,
// позицией и длиной
 Procedure SetSize(size:integer);// изменение размера
 Procedure Drive; // управление змейкой
 Procedure Update; // один ход змейки
 Procedure Draw; // отрисовка змейки
Destructor Free; // удаление змейки
end;

Как и в любой игре, в этой нельзя обойтись без математики. Тип «Vect2D» облегчает нам задачу хранения координат и взаимодействия с ним:

type
vect2D = record
 X,Y: integer;
end;
Function v2(x,y:integer):vect2D; // создание
begin
 result.X:= X;
 result.Y:= Y
end;
Function v2ident(v1,v2:vect2D):boolean; // проверка одинаковых координат
begin
 result:= false;
 if (v1.x=v2.x) and (v1.y=v2.y) then result:=true
end;
Function v2add(v1,v2:vect2D):vect2D; // сложение координат
begin
 result.X:= v1.x+v2.x;
 result.Y:= v1.y+v2.y
end;

И вот код всех процедур класса:

// мы создаем змейку
Constructor TSnake.Create(spr_name:string;pos:vect2D;size:integer);
Begin
 Sprite:= TBitmap.create; // загружаем спрайт
 Sprite.LoadFromFile(spr_name);
 Sprite.Transparent:= true;
 SetSize(size); // установка длины
 BodyPos[0]:= pos; // установка положения
 MoveTo:= left; // изначальное направление
end;
// установка длины массива частей змейки:
Procedure TSnake.SetSize(size:integer);
begin
 len:=size;
 SetLength(BodyPos, len)
end;
// вот тут самое сложенное и интересное. Относительно MoveTo устанавливается скорость змейки
// и происходит сдвиг змейки в сторону движения
procedure TSnake.Update;
var i: integer;
begin
 if MoveTo=Up then Speed:=v2( 0,-1);
 if MoveTo=Down then Speed:=v2( 0, 1);
 if MoveTo=Left then Speed:=v2(-1, 0);
 if MoveTo=right then Speed:=v2( 1, 0);
 // двигаем хвост (downto означает, что мы идет от большего к меньшему)
 for i:= len-1 downto 1 do
  BodyPos[i]:= BodyPos[i-1];
 // двигаем первый элемент (голову) прибавляя скорость
BodyPos[0]:= v2add(BodyPos[0], Speed);
// проверка столкновения головы змейки с хвостом
for i:=1 to len-1 do if v2Ident(BodyPos[0],BodyPos[i]) then TryAgain;
end;

TryAgain() – процедура вызываемая при проигрыше. К ней вернемся позже. В следующей процедуре мы управляем змейкой, задавая ее направление движения через MoveTo(). Это отдельная от Tsnake.Update процедура, поскольку Update() будет вызываться реже таймера, чтобы контролировать скорость движения змейки. При этом, ловить нажатые клавиши управления надо постоянно. Определение нажатых клавиш происходит через функцию Key_Press():

function Key_Press(key: byte): boolean;
var
  keys: TKeyboardState;
begin
  result:=false;
  GetKeyboardState(keys);
  If (keys[key] = 128) or (keys[key] = 129) then result:= true
end;

Коды клавиш можно определить специальной программой [2]. Те, что нам понадобятся, я занес в константы:

KEY_UP= 38;
KEY_DOWN= 40;
KEY_LEFT= 37;
KEY_RIGHT= 39;
KEY_ESC= 27;

Поскольку змейка может только поворачивать влево и вправо, а не может изменять направления движения мгновенно на 180 градусов, введены проверки типа v2Ident(v2(BodyPos[0].x,BodyPos[0].y-1),BodyPos[1])=false, которые не позволяют ей этого сделать:

procedure TSnake.Drive;
begin
 if Key_Press(KEY_UP) then if v2Ident(v2(BodyPos[0].x,BodyPos[0].y-1),BodyPos[1])=false then MoveTo:=Up;
 if Key_Press(KEY_DOWN) then if v2Ident(v2(BodyPos[0].x,BodyPos[0].y+1),BodyPos[1])=false then MoveTo:=Down;
 if Key_Press(KEY_LEFT) then if v2Ident(v2(BodyPos[0].x-1,BodyPos[0].y),BodyPos[1])=false then MoveTo:=Left;
 if Key_Press(KEY_RIGHT) then if v2Ident(v2(BodyPos[0].x+1,BodyPos[0].y),BodyPos[1])=false then MoveTo:=Right;
end;

И две последние процедуры. Отрисовка отобразит все части змейки по координатам (голова отрисовывается два раза для наглядности):

procedure TSnake.Draw;
var i: integer;
begin
for i:=0 to len-1 do if not v2Ident(BodyPos[i],v2(0,0)) then begin
// не отрисовываются части с нулевыми координатами,
// так как их имеют новые части змейки до движения
if i=0 then scr_Buffer.Canvas.draw(G_X(BodyPos[i].x)+Speed.x*5,G_Y(BodyPos[i].y)+Speed.y*5,sprite);
scr_Buffer.Canvas.draw(G_X(BodyPos[i].x),G_Y(BodyPos[i].y),sprite);
end
end;
// при удалении змейки очищается спрайт
destructor TSnake.Free;
begin
Sprite.Free;
end;

Рис. 2. Отображение нашей змейки

Объекты в игре. Не дадим змейке «помереть с голоду»

Теперь необходимо добавить объекты. Они будут двух типов:

Type
TObjType=(oWall,oFood); // стены и еда
type
TgameObject = class
 ObjType:TObjType; // тип объекта
 Sprite:TBitmap; // спрайт
 Pos:vect2D; // положение
Constructor Create(spr_name:string;oType:TObjType;p:vect2D); // создание
 procedure Update(Snake:TSnake); // обновление (проверка столкновения со змеей)
procedure Draw; // отрисовка
destructor Free; // удаление
end;

Тут ничего сложного нет. Многие процедуры аналогичны змейке:

Constructor TGameObject.Create(spr_name:string;oType:TObjType;p:vect2D);
begin
 Sprite:=TBitmap.create;
 Sprite.LoadFromFile(spr_name);
 Sprite.Transparent:=true;
 pos:= p;
 ObjType:= oType
end;
Procedure TGameObject.Update(Snake:TSnake);
begin
 if v2Ident(Snake.BodyPos[0],pos) then begin // если змея столкнулась с объектом
 if ObjType = oWall then TryAgain;
 if ObjType = oFood then begin Snake.SetSize(Snake.len+1);
 pos:= RandomFieldPos
end;
 // увеличиваем размер змеи, перемещаем еду в другое место
 // что такое RandomFieldPos см. ниже
end;
end;
Procedure TGameObject.Draw;
begin
 scr_Buffer.Canvas.Draw(G_X(pos.x),G_Y(pos.y),sprite)
end;
Destructor TGameObject.Free;
begin
 Sprite.Free
end;

Используем классы в игре

Итак, компоненты игры написаны. Теперь их надо создать, добавить обработку и отрисовку в Main_Update() и Main_Draw(). Вот полный код инициализации змейки и стен. Объявим следующие переменные:

MySnake:TSnake; // змейка игрока
Wall: array of TGameObject; // все стены игры
WallNum:integer; // их кол-во
Food:TGameObject; // еда для змейки

И добавим в инициализацию следующий код:

// создаем змейку MySnake
MySnake:= TSnake.Create(‘snake.bmp’,v2(7,7),2);
// теперь создам еду. А на ужин у нас аппетитненький пингвинчик
Food:=TGameObject.Create(‘food.bmp’,oFood,RandomFieldPos);

Чтобы не вводить координату каждой стенки уровня, так как загрузчика уровней нет, я просчитал создание всех стен в операторе for:

WallNum:=54;
SetLength(Wall,WallNum);
for i:=0 to 15 do Wall[i] := TGameObject.Create(‘wall.bmp’, oWall, v2(i,0)); // верхняя
for i:=16 to 31 do Wall[i]:= TGameObject.Create(‘wall.bmp’, oWall, v2(i-16,11)); // нижняя
for i:=32 to 42 do Wall[i]:= TGameObject.Create(‘wall.bmp’, oWall, v2(0,i-32)); // левая
for i:=43 to 53 do Wall[i]:= TGameObject.Create(‘wall.bmp’, oWall, v2(15,i-43)); // правая

Выше я писал о функции RandomFieldPos(), которая возвращает случайную координату на поле, где нет стен. В OnCreate() надо поставить randomize() для инициализации генератора случайных чисел:

function RandomFieldPos:vect2D;
begin
 result:=v2(random(13)+1,random(9)+1); // я просчитал допустимые значения
// по X от 1 до 14, по Y от 1 до 10
// тут ничего сложного нет.
end;

Собираем запчасти

Теперь надо добавить нашу змейку, стены и пингвинчика в обработку и отрисовку. Поскольку скорость движения змейки надо ограничить, мы заводим переменную-счетчик WaitTime. Она считает до 5 и выполняет процедуру движения и сбрасывает себя на 0. В итоге, MySnake.Update() срабатывает в 5 раз реже таймера. Ошибкой многих начинающих разработчиков является использование большого количества таймеров, что сильно усложняет код. Чтобы из игры можно было нормально выйти, сделаем условие нажатия на клавишу <ESCAPE>:

procedure Main_Update;
var i: integer;
begin
 MySnake.Drive;
 for i:=0 to WallNum-1 do wall[i].Update(MySnake);
Food.Update(MySnake);
if WaitTime>5 then begin MySnake.Update; WaitTime:= 0; end else inc(WaitTime);
if Key_Press(KEY_ESC) then Application.Terminate
end;
procedure Main_Draw;
var i: integer;
begin
scr_Buffer.Canvas.Draw(0, 0, BackImage);
MySnake.Draw;
for i:=0 to WallNum-1 do Wall[i].Draw;
Food.Draw;
MainForm.Canvas.Draw(0, 0, scr_Buffer);
scr_Buffer.Canvas.Rectangle(0, 0, SCR_WIDTH, SCR_HEIGHT);
end;

Напишем «правильное очищение» всех ресурсов игры:

procedure TMainForm.FormDestroy(Sender: TObject);
var i: integer;
begin
 scr_Buffer.Free;
BackImage.Free;
MySnake.Free;
for i:=0 to wallnum-1 do wall[i].Free
end;

После чего остается процедура «проигрыша» TryAgain(). В ней сбрасывается длина змейки, а сама она перемещается на стартовую позицию:

procedure TryAgain; // сброс всех координат
begin
 MySnake.SetSize(0);
 MySnake.SetSize(2);
 MySnake.BodyPos[0]:= v2(7,7);
 MySnake.MoveTo:= left;
 Food.Pos:=RandomFieldPos
end;

Размер устанавливается два раза: первый для того, чтобы сбросить координаты частей змейки на нулевые значения, второй для установки начальной длины. И вот результат (см. рис.3):

Рис. 3. Результат наших трудов. Любуемся игрой

Заключение

Теперь можно запустить игру и наслаждаться результатом. Посмотрев на финальный код можно увидеть, что разработка простой игры не требует каких-либо особых усилий и знаний. Мной эта игра для статьи была написана за 2-3 часа. Весь исходный код проекта, а также текстуры, используемые для создания игры можно найти на http://www.programmersforum.ru в разделе «Журнал клуба программистов. Второй выпуск».

Обсудить на форуме – GAMEDEV на Delphi. Делаем змейку

Статья из второго выпуска журнала «ПРОграммист».

Школа программирования Delphi

Портал DelphiSchool является бесплатным проектом, обеспечивающим пользователям быстрый и легкий доступ к урокам программирования на Delphi. Сайт позволяет научиться программировать на Делфи любому, кто хочеть писать свои программы, игры, Android приложения, программы для MAC OC или IOS. Кроме уроков Delphi и статей Delphi, на сайте доступны также и видеоуроки. Практически к каждому уроку, пользователю доступен исходник, изучив который, он сможет наглядно посмотреть как работает та или иная программа, написанная на Делфи. Кроме того мы постараемся прилагать к каждому материалу (статье, уроку, видеоуроку) файлы Delphi, которые будут помогать изучить предоставленный материал.

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

DelphiSchool

Delphi игра — урок по созданию простой игры. Часть №1.

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

Если захотите узнать стоимость SEO услуг, то посетите сайт netsend.ru.

Как всегда открываем Delphi и создаем новый проект. Сразу кидаем на форму компонент DrawGrid с закладки Additional. У него нам нужно изменить ряд свойств:

1. Свойство ColCount — сделайте равным 20
2. Свойство RowCount — тоже 20
3. Свойство DefaultColWidth = 20
4. Свойство DefaultRowHeight = 20
5. FixedCols = 0
6. FixedRows = 0
7. DefaultDrawing = False

В итоге у вас должно получиться что то похожие.

delphi game

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

Переходим в код и выше ключевого слова var вставляем вот такой код

const
MAX_COLORS = 3;
const
PossibleColors : array [0..MAX_COLORS-1] of TColor = (clRed, clBlue, clGreen);

Далее, нам понадобиться создать двухмерный массив, в ячейках которого будут храниться цвета соответствующих квадратов. т.к. размер игровой области у нас 20 на 20, соответственно и массив у нас будет такой же размерности. Но для начала давайте объявим парочку констант в которых будет храниться размер игровой области. Опять же над ключевым словом var напишем следующий код:

Const
MAX_COLUMNS = 20;
MAX_ROWS = 20;

А теперь создадим массив. Делать мы это будем после ключевого слова var т.е. сразу же после:

var
Form1: TForm1;

Пишем:

ColorOfBrick : array [0..MAX_COLUMNs, 0..MAX_ROWS] of TColor;

Двигаемся дальше, сейчас нам нужно создать процедуру которая будет при запуске игры случайным образом назначать квадратам соответствующие цвета. После ключевого слова public пишем

procedure AssignBrickColors;

Нажимаем комбинацию клавиш CTRL+SHIFT+C …вуаля и Delphi создал шаблон для нашей будущей процедуры. В дальнейшем я не буду подробно рассказывать о процессе создания новой процедуры или функции, если вы вдруг что ни будь забудете вернитесь на это место. Теперь когда шаблон для процедуры у вас уже создан посмотрите на то что должно из неё получиться в итоге и добавьте недостающие строчки.

procedure TForm1.AssignBrickColors;
var
i, j : integer;
begin
for i := 0 to Form1.DrawGrid1.ColCount-1 do
for j := 0 to Form1.DrawGrid1.RowCount-1 do
ColorOfBrick[i][j] := PossibleColors[Random(MAX_COLORS)]
end;

Справились ? я надеюсь что да ! Давайте дадим возможность игроку начинать новую игру тогда когда он этого захочет, для этого киньте на форму компонент Button (кнопка) с закладки Standard и сразу же измените у него свойство Caption на «Новая Игра». Создаем обработчик событий OnClick на кнопке, кликнув на ней два раза. Между begin и end пропишите:

AssignBrickColors;

Конечно же при запуске, также необходимо начинать новую игру, поэтому создаем обработчик событий onCreate на форме, опять же между begin end пишем:

Randomize;
Button1.Click;

Ну и напоследок создайте обработчик событий OnDrawCell на компоненте DrawGrid напишите там

DrawGrid1.Canvas.Brush.Color := ColorOfBrick[ACol][ARow];
DrawGrid1.Canvas.FillRect(Rect)

Запускаем проект, вот что получилось у меня:

delphi игра

Конец 1-ой части

Похожие материалы

  • Delphi уроки — создаём простую игру.Часть 2
  • Ошибки которые отнимают у вас прибыль
  • Быстрый поиск исходников, Delphi статьи
  • Продавать свои программы ВОЗМОЖНО !
  • Дизайн ваших продающих сайтов

Последние из рубрики

  • Delphi Уроки: компонент ProgressBar внутри компонента ListView
  • Delphi уроки — Запрещаем форме уезжать за пределы экрана
  • Ураааа 2009 !!!

Создание игры Пятнашки


Источник: mycomp.com.ua

Оформил: RT17

Можно ли в Delphi создать что-нибудь непохожее на базы данных? «Нет! — ехидно скажут программисты на Си, — Все непохожее на базы данных пишеться на СиСи+». Хотя это еще как сказать. Мне несколько раз подряд попадались исходные тексты некоторых игр, «написанные на Си», в которых самого Си было максимум процентов 5-10, а все остальное — чистой воды Ассемблер! Случайность это или все-таки закономерность? На мой взгляд, с таким же успехом можно использовать связку Delphi-Ассемблер. Тем более, что в Delphi есть все для создания крупномасштабных проектов, в том числе и игр (например, поддержка OpenGL — для работы с 3D-графикой; OpenGL, кстати, использовался при создании Quake III). Я, конечно, не собираюсь в этой статье рассказывать, как создать в Delphi Quake III (не по мне такие задачи). Речь пойдет о более приземленных вещах. А именно, о маленькой простой игре, которой можно дополнить набор мелкомягких игр, устанавливаемых вместе с Windows. Эта игра в народе называется «пятнашки» — очень популярная раньше настольная (вернее даже, наручная) игра, которая продавалась в квадратных коробочках с большой цифрой 15 на крышке, в которой нужно было расставить квадратики с числами в порядке от 1 до 15. Ну что, вспомнили? Нет!?… Да, трудное у вас было детство… Ну да ладно. Итак, значит, будем писать «пятнашки» в Delphi.

Для начала приступим к созданию интерфейса. Здесь все полностью зависит от вашей фантазии. Но я остановлюсь на праздном сером оформлении (см. Рис. 1). Теперь о том, как получить такой образец серости и примитивизма. Сначала на форме располагается компонент TPanel со свойствами BevelInner и BevelOuter, равными bvLowered, для создания эффекта бордюра по краям формы. Затем на полученную панель ставится еще одна панель меньшего размера со свойством BevelOuter равным bvRaised, BevelInner —bvLowered, а цвет Color —clBlack. Эта вторая панель будет фоном для кнопок с цифрами. Затем добавляются кнопки (компоненты TButton или TSpeedButton) с названиями about, game, exit и кнопка начать игру (компонент TSpeedButton). Расположение их показано на рисунке. Теперь надо создать те самые квадратики с цифрами. Эту роль играют компоненты TButton. Расположите их на второй панели именно так, как показано на рисунке, то есть, кнопка с цифрой (Caption) 1 должна иметь имя (Name) Button1, кнопка 2 —Button2 и т.д. Это важно. Объясняю, почему. При добавлении компонента на форму он автоматически заносится в список (массив) компонентов формы и получает индекс начиная с 0. В дальнейшем взаимодействие с кнопками программой будет осуществляться через их индексы. Поэтому если у вас кнопка с именем Button1 будет иметь Caption 2, вам просто будет сложнее работать с ней. Что касается размеров кнопок, то я установил параметры Heigth и Width каждой по 50. Да, еще. Чтобы посмотреть индекс кнопки, размещенной на форме, в ее процедуре-обработчике события (Event), например, OnClick, наберите:

form1.caption:=inttostr((sender as tbutton).componentindex);

Это приведет к тому, что при нажатии на кнопку ее индекс будет выводиться в заголовке формы. Потом эту строчку можете удалить.

Теперь, когда интерфейс программы готов, можно перейти непосредственно к программированию. Опишем глобальные переменные модуля. В разделе var (там, где написано Form1: TForm1) напишите:

a:array[1..16]of byte;
i,k,fl,rn,p,m:byte;
x,y,x1,y1,num,pos,lr,td,lr1,td1:integer;
flag:boolean;

(назначение переменных я буду объяснять далее). После этого нужно написать процедуру, генерирующую массив случайных чисел от 1 до 16 так, чтобы они не повторялись. Потом по этому массиву будут расставляться кнопки с числами. Случайные числа будем заносить в массив a. Цифра 16 будет означать пустую область, на которой нет кнопки. Процедура заполнения массива случайными числами выглядит следующим образом:

procedure rndarr; 
begin
 for k:=1  to 16 do a[k]:=0;
 randomize;
 i:=1;
repeat
 rn:=random(16)+1;
 fl:=0;
 k:=1;
 while (a[k]<>rn) and (k<>17) do inc(k);
 if k=17 then begin a[i]:=rn;
 Inc(i);
 end;
until i=17;
end;

Обращаю ваше внимание на то, что приведенная выше процедура не является обработчиком какого-либо события, поэтому не нужно ее объявлять в интерфейсной части модуля. Просто наберите ее как есть после Implementation {$R *.DFM}. Такие процедуры называются пользовательскими. (Для того чтобы узнать, какие числа появляются в массиве после выполнения этой процедуры, можно использовать окно Watch).

Визуализация массива

На этом принципе работают многие игры. Например, тот же тетрис: имеется некий двумерный массив (стакан), в котором нули — пустые позиции, единицы — квадратики, из которых строятся фигуры тетриса. Далее в массиве эти единицы сдвигаются, и массив выводится на экран с заменой 0 и 1 на графические элементы. Это повторяется несколько раз, что создает эффект падения фигур. В данном случае все должно происходить аналогичным образом: все перемещения производятся в массиве, а в соответствии числам из массива располагаются сами «пятнашки». Для того чтобы это осуществить, нужно написать процедуру, в которой читаются элементы массива, соответствующие индексам компонентов-кнопок с цифрами. Затем происходит обращение к этим кнопкам по их индексу и размещение их соответственно значениям в массиве. То есть, если первым элементом массива является число 15, то первой кнопкой в левом верхнем углу будет кнопка с цифрой 15 и т.д. Вот эта процедура (наберите ее после первой):

procedure drawarr;    
begin
 p:=0;
 for i:=0 to 3 do
 for k:=0 to 3 do
 begin
  p:=p+1;
 if a[p]<>16 then
  begin
  with TButton(form1.components[a[p]+5]) do
   begin 
   left:=k*50+2;
   top:=i*50+2;   
   end;
  end;
 end;
end;

Так как у меня первая кнопка в массиве компонентов формы имеет индекс 6 (:-)), то я при обращении к кнопкам прибавляю к значению из массива число 5, чтобы получить их индексы:

TButton(form1.components[a[p]+5]));

Кнопки имеют размер 5050, первая из них расположена правее на 2 пикселя от левого края черной (второй) панели и на 2 пикселя ниже от верхнего ее края, поэтому, чтобы правильно их расположить, будем умножать переменные i и k на 50 и прибавлять 2. Таким образом, если, например, i и k равны 0, то координаты первой кнопки в левом верхнем углу, по отношению к черной панели, равны (2,2), если i=0, k=1, то координаты —(2,52), и т.д.

Начать игру

Начнем работу с процедурами-обработчиками событий. Первая из них — обработчик события OnClick кнопки (TSpeedButton) «Начать игру». Дважды щелкните по этой кнопке. При этом откроется редактор кода с таким заголовком процедуры:

procedure TForm1.SpeedButton2Click(Sender: TObject);
begin
rndarr;
drawarr;
form1.speedbutton2.caption:='Начать игру заново';
end;

Таким образом, при нажатии на кнопку «Начать игру» происходит генерация массива случайных чисел процедурой rndarr, затем — размещение кнопок с цифрами соответственно числам в массиве процедурой drawarr и, наконец, изменение названия кнопки Начать игру на Начать игру заново. Теперь можете запустить программу и поклацать кнопкой «Начать игру».

О кнопках с цифрами

Предпоследний этап. Нужно сделать так, чтобы при нажатии на кнопку с цифрой определялось направление ее движения, т.е. то место, где нет другой кнопки. Для этого клацните дважды, например, по кнопке 15 и в появившейся процедуре-обработчике события OnClick (как и в предыдущий раз) между Begin и End наберите:

if flag then exit; {если flag=true — выход из процедуры}
pos:=0;m:=0;num:=0;
num:=(sender as tbutton).componentindex-5; {num — номер нажатой кнопки}
for i:=1 to 16 do if a[i]=num then pos:=i; {определение ее позиции в массиве}
{определение направления движения}
if (pos-1>0)and(pos-1<>4)and(pos-1<>8)and(pos-1<>12)and(a[pos-1]=16)then m:=1;
 if (pos+1<17)and(pos+1<>5)and(pos+1<>9)and(pos+1<>13)and(a[pos+1]=16)then m:=2;
 if (pos-4>0)and(a[pos-4]=16)then m:=3;
 if (pos+4<17)and(a[pos+4]=16)then m:=4;
 if m=0 then exit; {если вокруг кнопки пустой позиции нет — выход}
flag:=true; {установливаем флаг, означающий, что кнопка в движении}
lr1:=(sender as tbutton).left; {сохраняем в lr1 и td1 начальные координаты}
td1:=(sender as tbutton).top;
lr:=0;td:=0;
form1.move(sender); {вызов процедуры перемещения кнопки}

В Object Inspector в разделе Events для остальных кнопок с цифрами напротив события OnClick укажите эту процедуру. Таким образом, она будет выполняться при нажатии любой кнопки с цифрой. В этой процедуре определяется, есть ли рядом с нажатой кнопкой пустая позиция. Если таковая слева, то m:=1, справа —m:=2, сверху —m:=3, снизу —m:=4. Определение «слева или справа» происходит путем вычета или прибавления к позиции нажатой кнопки единицы, определение «сверху или снизу» — через вычет или прибавление 4. Переменная Flag служит для того чтобы определить, движется ли какая-либо кнопка или нет. Если движется — процедура не должна выполняться. Процедура перемещения кнопки form1.move описана далее.

Двигай кнопкой

Чтобы как-то «оживить» игру, требуется движение. В данном случае — движение кнопок. Для реализации заметного человеческому глазу и более-менее плавного движения потребуется компонент TTimer. Выберите его из списка компонентов и расположите в любом месте формы. Установите его свойство Interval равным 1. Затем в его единственном событии OnTimer в Object Inspector напишите move и нажмите Enter. На экране появится тело процедуры-обработчика этого события:

procedure TForm1.move(Sender: TObject);
begin
timer1.enabled:=true; {включение таймера}
case m of  {исходя из направления движения}
1:dec(lr,5); {уменьшаем на 5 lr}
2:inc(lr,5); {увеличиваем на 5 lr}
3:dec(td,5);
4:inc(td,5);
end;
with TButton(components[num+5])do begin {перемещаем компонент}
left:=lr1+lr;top:=td1+td;end;
if (abs(lr)=50) or (abs(td)=50) then  {когда пройдено 50 шагов}
 begin
  timer1.enabled:=false; {выключаем таймер}
  lr:=0;
  td:=0;
  flag:=false;
  case m of  {перестановка чисел в массиве}
  1:begin a[pos-1]:=a[pos];a[pos]:=16;end;
  2:begin a[pos+1]:=a[pos];a[pos]:=16;end;
  3:begin a[pos-4]:=a[pos];a[pos]:=16;end;
  4:begin a[pos+4]:=a[pos];a[pos]:=16;end;
  end;
 fl:=0;
 for i:=1 to 16 do if a[i]<>i then fl:=1; {определяем, расставлены ли кнопки по порядку от 1 до 15}
 if fl=0 then showmessage('Вы выиграли!'); {если кнопки расставлены как надо — сообщение «Вы выиграли!»}
 end;

end;

Основная часть программы уже написана. Осталось внести последние штрихи. Создайте форму AboutBox и в процедуре-обработчике события OnClick кнопки About напишите: AboutBox.Show. Аналогично, для кнопки Exit напишите Close — для выхода из программы. Да, еще. В Object Inspector в параметре формы BorderStyle выберите bsDialog — форму с таким стилем невозможно развернуть или изменить ее размер. Также, по желанию, можно сделать меню (компонент TPopUpMenu) для кнопки Game.

Понравилась статья? Поделить с друзьями:
  • Как написать игру марио
  • Как написать игру майнкрафт
  • Как написать игру крестики нолики на питоне
  • Как написать игру крестики нолики на python
  • Как написать игру кликер на python