Движок 2D-графики Asphyre Sphinx. Урок 2: выводим картинку на форму, создаём контейнер ASDb

На урок 1 Урок 2 На урок 3

   Из трудовых будней начинающего программиста графики.
   Представьте себе, что мы начинаем писать игру, используя новую технологию вывода графики. Пусть это будет что угодно… к примеру OpenGL или DirectX или, в конце концов, движки типа GLScene и Asphyre Sphinx. Сначала нас вдохновляет описание работы с технологией – судя по этому документу, работа с графикой станет нереально простой и гибкой, можно будет делать различные навороты. Эти феерические утверждение подкреплены примерами, в которых есть и свет, и какие-то трёхмерные фигуры метко взаимодействующих с друг другом и средой, обязательно вода. Обязательно что-то крутится и вертится и всё это впечатляет.
   Такое положение вдохновляет на скорейшее начало работы над игрушкой, и, мы, напрочь забыв про проектирование, берёмся за дело. С чего мы начинаем писать игру? Обычно с файловой системы, я полагаю. Дело доходит до графики, в среднем не раньше чем через неделю, и тут то вопрос встаёт ребром. Нужно разобраться с разрешением экрана, с тем, как инициализировать графику, задать формат пикселя, нужно загрузить все текстуры, нужно написать процедуру вывода всей графики и т.п. Итого в сумме десяток вопросов(вопросы верхнего уровня), с которыми нужно разобраться. Каждый этот вопрос состоит из других 10 вопросов(вопросы второго уровня). Каждый этот вопрос в свою очередь также содержит 10 более простых базовых  практических вопросов(вопросы нижнего уровня). В итоге мы получаем проблемы, которая требует найти ответ на 1000 вопросов. Кто с таким не сталкивался?
   Мы разбираемся с графикой, начинаем решать вопрос за вопросом, в надежде на некий прорыв, который произойдёт вот-вот. Очень хочется писать игру дальше. И вот случается неприятность — неожиданно тормозим на одном из вопросов, ничего не получается и не работает. Начинаем копать примеры, и это также не приводит к успеху и уже раздражает. Примеры кажутся совершенной возмутительной бессмыслицей. Потом мы пишем знакомому, который разбирается в этом движке(у всех всегда есть такой знакомый). Он помогает нам один-два раза, а на третий раз отправляет к своим программам, которые также кажутся бессмысленными, ещё хуже, чем примеры. И вот тут пойди-разберись, как оно работает? Ну не понятно и всё…
   Эта ситуация встречается очень часто по двум причинам – отсутствие “человеческой” документации (действует миф о том, что программистам не нужна вменяемая документация, программисты любят сложности) и игнорирования нами этапа проектирования. Всё будет гораздо легче, если мы ещё при проектировании очертим тот круг возможностей, которые нам нужны для игры и начнём разбираться по ранее продуманному плану. Если даже документацию нельзя найти, то смерти проекта можно избежать, составив план изучения графики.

   Прошу простить за столь большое вступление и за слишком подробное изучение вопросов, но, на мой взгляд, это просто необходимо, чтобы вникнуть в Asphyre Sphinx при первом же столкновении с реальной задачей.
   Мы будем изучать вместе движок Asphyre Sphinx очень постепенно и очень подробно, чтобы избежать недосказанностей, которыми пестрят различные мануалы.
   Итак, сегодня нам нужно вывести картинку на экран средствами Asphyre Sphinx. Для этого нужно будет разобраться в формате ASDb, точнее только начать разбираться. Подробнее про ASDb будет в последующих уроках.

   Для начала создайте папку для нашего проекта. У себя я назвал её, как “DrawtexEx”, что можно расшифровать, как “пример рисования текстур”
Создатели движка Asphyre Sphinx настаивают на использовании их формата-контейнера(архива) для хранения различных файлов, которые будут использоваться в вашей программе. Речь идёт о файлах ADSb. Так давайте и мы не будем пытаться загружать изображение непосредственно из файлов на диске(по крайней мере в начале изучения), воспользуемся для этого технологией ADSb.
   Сначала поместим нужную картинку в файл ASDb посредством утилит Asphyre Sphinx. В пакете поставки Asphyre Sphinx есть специальные программы, которые нужны при работе с движком – это утилиты. Находятся они в папке Tools, которая лежит в папке поставки AsphyreSphinx100. Про то, как и где скачать движок говорилось в первом уроке.

   Запустим файл AsphyreManager.exe из этой папки – эта утилита для работы с ASDb файлами.  Когда писалась статья, утилита Asphyre Manager была в версии 2.21.
   Для того, чтобы начать работу с файлом ASDb нужно его создать, либо загрузить. Давайте создадим новый ASDb файл. Для этого нужно нажать кнопку с чистым листочком “Create a new ASDb archive”, вверху справа.  Сохраните новый файл в созданную вам папку. Я файл назвал ex1.asdb, что значит пример 1 – не особо напрягался над выдумыванием названия. Когда будете писать игры, помните, что в игре должны быть названия осмысленны!
   Обратите внимание, что после создания файла часть кнопок стала активной. Давайте добавим нужное изображение. Я использую для наглядности Лену из примера basic(вращающаяся Лена), правда в нашем примере девушка вращаться не будет. Файл с картинкой лежит в папке с примером в формате PNG, нам нужно добавить её в наш ASDb архив ex1.asdb. Для этого нажмём кнопку, где нарисована картинка и плюсик ”Add a single image witch will be used in 2D rendering to the ASDb archive». В открывшемся окне нажмём кнопку browse и выберем папку с примером basic. Напомню путь AsphyreSphinx100ExamplesDelphiBasic. Выберем файл lena.png. Программа сразу определит размер текстуры, а тут он внушителен 512×512. Возможности этого окна мы разберём позже, сейчас обратите внимание на имя картинки, которое будет у этой картинки Key indefender: lena.image. Это имя нужно будет знать для доступа к картинке из нашего delphi-проекта. Жмите кнопку ADD. Картинка появилась в списке файлов. Программу можно закрыть, файл ex1.asdb уже содержит картинку и готов к использованию.
   Такие ASDb архивы очень удобны для хранения какой-то тематической информации, например всех текстур конкретного героя, монстра или объекта, либо все текстуры одного уровня(стаж, этап в игре), или вообще все файлы одного уровня.

   Перейдём к самому интересному – к первой программе на Asphyre Sphinx!
   Создадим новый проект в Делфи. File->New->VCL Form Application.
Для начала, как это обычно положено у уважаемых модулей, нужно кое-что инициализировать при запуске. Для этого у формы Form1 создадим событие OnCreate. В нём запишем следующее:

  Factory.UseProvider(idDirectX7);

   Для того, чтобы эта строка работала, необходимо подключить сразу два модуля — DX7Providers, AsphyreFactory(Строка вверху модуля USES, т.е. использовать – туда через запятую вписать два новых модуля). AsphyreFactory – модуль ”фабрика Asphyre” – это своеобразное ядро движка Asphyre Sphinx, к нему ещё придётся вернуться и не раз.  А вот модуль DX7Providers обеспечивает работу с движком DirectX 7.
   Строка Factory.UseProvider(idDirectX7) означает то, что мы даём команду программе в дальнейшем использовать движок DirectX 7! Впечатляет то, что стоит вместо idDirectX7 написать idOpengl, то наша программа будет использовать движок OpenGL, а если использовать idDirectX9, то программа будет использовать движок DirectX 9! Замечу, что для использования OpenGL нужно подключить модуль  OGLProviders, а для работы с DirectX 9 модуль DX9Providers. Я сравнил работу примеров на различных движках(на примере Basic). Быстрее всего DirectX 7, медленнее на 3% DirectX 9, ещё чуть медленней OpenGL. Впрочем разница в скорости малосущественна, но в примерах мы будем пользоваться DirectX7(Для этого у вас должна стоять библиотека DirectX7 или выше)!
   Запишем в обработчик события OnCreate следущее:

DisplaySize:= Point2px(ClientWidth, ClientHeight);

   Для этого подключим модуль Vectors2px(строка Uses) и объявим переменную DisplaySize(переменная типа TPoint2px) после директивы implementation(Эту переменную мы будем использовать только в модуле главной форме, поэтому делаем её локальной для главного модуля): var DisplaySize: TPoint2px;
   Переменная нужна для того, чтобы программа знала какой у нас размер активного экрана. ClientWidth и ClienthHeight – это ширина и высота формы, без учёта её обрамления, то есть активная область формы. Тип TPoint2px содержит координаты точки, то есть x и y. К ним можно обращаться вручную, как DisplaySize.x и DisplaySize.y, но удобнее обращаться методами, предусмотренными движком Asphyre Sphinx.
  Добавим в обработчик OnCreate

GameDevice:= Factory.CreateDevice();
GameCanvas:= Factory.CreateCanvas();
GameImages:= TAsphyreImages.Create();
GameDevice.Initialize();//Эта вещь инициализирует устройство вывода

Объявим соответственные переменные в var

GameDevice : TAsphyreDevice = nil;
GameCanvas : TAsphyreCanvas = nil;
GameImages : TAsphyreImages = nil;

   А также добавим модуль AbstractDevices, , AbstractCanvas, AsphyreImages в Uses. Сейчас мы инициализировали устройство вывода, канву и изображения в Asphyre Sphinx. Об этих модулях поговорим позже. Скажу только, что они нужны для указания, куда выводить графику. Ну а модуль AsphyreImages, конечно же, нужен для хранения картинок-текстур.
   Закончим основную инициализацию Asphyre Sphinx следующими строчками(всё в том же обработчике):

GameDevice.WindowHandle:= Self.Handle;
GameDevice.Size    := DisplaySize;
GameDevice.Windowed:= True;
GameDevice.VSync   := False;

   Это основные параметры устройства вывода. WindowHandle – ссылка на окно вывода. Size – размер окна. Windowed – оконный режим. VSync – вертикальная синхронизация(что это спросите у гугля, яндекса или википедии). Включенный Vsync сильно снижает скорость(частоту) вывода кадров, это факт я подтвердил экспериментально, при помощи примера “вращающаяся Лена”(basic). Других исследований в этой области я не проводил, поэтому если хотите поделиться своим опытом, то для этого существует специальная тема на нашем Форуме.

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

ImASDb  : TASDb = nil;

а в Uses допишем модуль, где описан тип TASDb – AsphyreDb. Теперь инициализируем переменную в событии формы OnCreate.

ImASDb:= TASDb.Create();//Создаём экземпляр объекта TASDb
ImASDb.FileName:= ExtractFilePath(ParamStr(0)) + ‘ex1.asdb’;//Путь к файлу + имя файла
ImASDb.OpenMode:= opReadOnly;//Режим работы с файлом – только для чтения

   Для начала создайте папку для нашего проекта. У себя я назвал её, как “DrawtexEx”, что можно расшифровать, как “пример рисования текстур”
Создатели движка Asphyre Sphinx настаивают на использовании их формата-контейнера(архива) для хранения различных файлов, которые будут использоваться в вашей программе. Речь идёт о файлах ADSb. Так давайте и мы не будем пытаться загружать изображение непосредственно из файлов на диске(по крайней мере в начале изучения), воспользуемся для этого технологией ADSb.
   Сначала поместим нужную картинку в файл ASDb посредством утилит Asphyre Sphinx. В пакете поставки Asphyre Sphinx есть специальные программы, которые нужны при работе с движком – это утилиты. Находятся они в папке Tools, которая лежит в папке поставки AsphyreSphinx100. Про то, как и где скачать движок говорилось в первом уроке.

   Запустим файл AsphyreManager.exe из этой папки – эта утилита для работы с ASDb файлами.  Когда писалась статья, утилита Asphyre Manager была в версии 2.21.
   Для того, чтобы начать работу с файлом ASDb нужно его создать, либо загрузить. Давайте создадим новый ASDb файл. Для этого нужно нажать кнопку с чистым листочком “Create a new ASDb archive”, вверху справа.  Сохраните новый файл в созданную вам папку. Я файл назвал ex1.asdb, что значит пример 1 – не особо напрягался над выдумыванием названия. Когда будете писать игры, помните, что в игре должны быть названия осмысленны!
   Обратите внимание, что после создания файла часть кнопок стала активной. Давайте добавим нужное изображение. Я использую для наглядности Лену из примера basic(вращающаяся Лена), правда в нашем примере девушка вращаться не будет. Файл с картинкой лежит в папке с примером в формате PNG, нам нужно добавить её в наш ASDb архив ex1.asdb. Для этого нажмём кнопку, где нарисована картинка и плюсик ”Add a single image witch will be used in 2D rendering to the ASDb archive». В открывшемся окне нажмём кнопку browse и выберем папку с примером basic. Напомню путь AsphyreSphinx100ExamplesDelphiBasic. Выберем файл lena.png. Программа сразу определит размер текстуры, а тут он внушителен 512×512. Возможности этого окна мы разберём позже, сейчас обратите внимание на имя картинки, которое будет у этой картинки Key indefender: lena.image. Это имя нужно будет знать для доступа к картинке из нашего delphi-проекта. Жмите кнопку ADD. Картинка появилась в списке файлов. Программу можно закрыть, файл ex1.asdb уже содержит картинку и готов к использованию.
   Такие ASDb архивы очень удобны для хранения какой-то тематической информации, например всех текстур конкретного героя, монстра или объекта, либо все текстуры одного уровня(стаж, этап в игре), или вообще все файлы одного уровня.

   Перейдём к самому интересному – к первой программе на Asphyre Sphinx!
   Создадим новый проект в Делфи. File->New->VCL Form Application.
Для начала, как это обычно положено у уважаемых модулей, нужно кое-что инициализировать при запуске. Для этого у формы Form1 создадим событие OnCreate. В нём запишем следующее:

  Factory.UseProvider(idDirectX7);

   Для того, чтобы эта строка работала, необходимо подключить сразу два модуля — DX7Providers, AsphyreFactory(Строка вверху модуля USES, т.е. использовать – туда через запятую вписать два новых модуля). AsphyreFactory – модуль ”фабрика Asphyre” – это своеобразное ядро движка Asphyre Sphinx, к нему ещё придётся вернуться и не раз.  А вот модуль DX7Providers обеспечивает работу с движком DirectX 7.
   Строка Factory.UseProvider(idDirectX7) означает то, что мы даём команду программе в дальнейшем использовать движок DirectX 7! Впечатляет то, что стоит вместо idDirectX7 написать idOpengl, то наша программа будет использовать движок OpenGL, а если использовать idDirectX9, то программа будет использовать движок DirectX 9! Замечу, что для использования OpenGL нужно подключить модуль  OGLProviders, а для работы с DirectX 9 модуль DX9Providers. Я сравнил работу примеров на различных движках(на примере Basic). Быстрее всего DirectX 7, медленнее на 3% DirectX 9, ещё чуть медленней OpenGL. Впрочем разница в скорости малосущественна, но в примерах мы будем пользоваться DirectX7(Для этого у вас должна стоять библиотека DirectX7 или выше)!
   Запишем в обработчик события OnCreate следущее:

DisplaySize:= Point2px(ClientWidth, ClientHeight);

   Для этого подключим модуль Vectors2px(строка Uses) и объявим переменную DisplaySize(переменная типа TPoint2px) после директивы implementation(Эту переменную мы будем использовать только в модуле главной форме, поэтому делаем её локальной для главного модуля): var DisplaySize: TPoint2px;
   Переменная нужна для того, чтобы программа знала какой у нас размер активного экрана. ClientWidth и ClienthHeight – это ширина и высота формы, без учёта её обрамления, то есть активная область формы. Тип TPoint2px содержит координаты точки, то есть x и y. К ним можно обращаться вручную, как DisplaySize.x и DisplaySize.y, но удобнее обращаться методами, предусмотренными движком Asphyre Sphinx.
  Добавим в обработчик OnCreate

GameDevice:= Factory.CreateDevice();
GameCanvas:= Factory.CreateCanvas();
GameImages:= TAsphyreImages.Create();
GameDevice.Initialize();//Эта вещь инициализирует устройство вывода

Объявим соответственные переменные в var

GameDevice : TAsphyreDevice = nil;
GameCanvas : TAsphyreCanvas = nil;
GameImages : TAsphyreImages = nil;

   А также добавим модуль AbstractDevices, , AbstractCanvas, AsphyreImages в Uses. Сейчас мы инициализировали устройство вывода, канву и изображения в Asphyre Sphinx. Об этих модулях поговорим позже. Скажу только, что они нужны для указания, куда выводить графику. Ну а модуль AsphyreImages, конечно же, нужен для хранения картинок-текстур.
   Закончим основную инициализацию Asphyre Sphinx следующими строчками(всё в том же обработчике):

GameDevice.WindowHandle:= Self.Handle;
GameDevice.Size    := DisplaySize;
GameDevice.Windowed:= True;
GameDevice.VSync   := False;

   Это основные параметры устройства вывода. WindowHandle – ссылка на окно вывода. Size – размер окна. Windowed – оконный режим. VSync – вертикальная синхронизация(что это спросите у гугля, яндекса или википедии). Включенный Vsync сильно снижает скорость(частоту) вывода кадров, это факт я подтвердил экспериментально, при помощи примера “вращающаяся Лена”(basic). Других исследований в этой области я не проводил, поэтому если хотите поделиться своим опытом, то для этого существует специальная тема на нашем Форуме.

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

ImASDb  : TASDb = nil;

а в Uses допишем модуль, где описан тип TASDb – AsphyreDb. Теперь инициализируем переменную в событии формы OnCreate.

ImASDb:= TASDb.Create();//Создаём экземпляр объекта TASDb
ImASDb.FileName:= ExtractFilePath(ParamStr(0)) + ‘ex1.asdb’;//Путь к файлу + имя файла
ImASDb.OpenMode:= opReadOnly;//Режим работы с файлом – только для чтения

GameImages.AddFromASDb(‘lena.image’,ImASDb, ‘lena’, True);//Загружаем изображение из ASDb файла

   Свойство OpenMode может принимать следущие сзначения: opUpdate –  для добовления файлов в коллекцию — , opOverwrite – для перезаписи, opReadOnly – только для чтения. В нашем примере, конечно, нужно только считать один файл, и поэтому используем только последний вариант.

  Не забудем и считать изображение из файла в память, и дать ему уникальный идентификатор ‘lena’

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

   Несомненно, для вывода картинки на экран необходим таймер(или нечто подобное), который будет ответственен за обновления изображения на форме. Такой таймер в Asphyre Sphinx есть, и описан он в модуле движка AsphyreTimer. Подключим этот модуль в Uses.
Давайте теперь в том же событии OnCreate, инициализируем таймер:

Timer.Speed    := 60.0;//Скорость, с которой будет идти обработка. От этого зависит скорость действия, но не зависит частота кадров
Timer.MaxFPS   := 4000;//Максимально-возможная частота кадров. Зачем? Непонятно
Timer.Enabled  := True;//Ага, включим таймер, чтобы работал

   Этого мало. Для того, чтобы таймер действительно работал, нужно описать  две процедуры. Одна процедура, которую таймер будет вызывать один раз в 60 тактов(у нас) – это в данном случае нам не нужно, и писать и изучать мы её будем в следующих уроках. Вторая процедура – процедура отрисовки. Вот такой таймер – 2 в 1, на самом деле удобно.
   Итак,  опишем процедуру отрисовки в разделе Private нашей From1.

    procedure TimerEvent(Sender: TObject);

Теперь вставим тело процедуры в блок Implementation:

procedure TForm1.TimerEvent(Sender: TObject);
begin
//
end;

Пока тело процедуры будет пустое
И опишем в OnCreate в свойство таймера это событие

Timer.OnTimer  := TimerEvent;

   Откомпилировав проект, можно убедиться, что он работает, хотя толку пока в нём нет никакого. Но ведь работает, а значит, добавив ещё чуть-чуть кода, мы увидим Лену с картинки!
   Приступим к выводу картинки. Нужно описать обработчик ещё одного события, предназначенного для вывода графики – RenderEvent. Событие RenderEvent – будет вызываться для отрисовки нашего изображения так часто, как только это возможно.
   Итак, опишем его в Private

procedure RenderEvent(Sender: TObject);

А теперь тело процедуры вставим в блок Implementation:

procedure TForm1.RenderEvent(Sender: TObject);
begin
//
end;

Чтобы это вызывалось, необходимо в TimerEvent добавить его вызов

procedure TForm1.TimerEvent(Sender: TObject);
begin
 GameDevice.Render(RenderEvent, $000000);
end;

   Render – это метод класса TAsphyreDevice, который указывает и вызывает метод отрисовки. Второе передаваемое значение указывает, какого цвета будет задний фон. $000000 – это чёрный. На самом деле ничего сложного нет, но, тем не менее, это всё может быть не понятно с первого раза. Призываю не переживать по этому поводу, всё быстро прояснится.

  Что впишем в процедуру отрисовки RenderEvent? Только то, что предусмотрено в Asphyre Sphinx для вывода изображения на форму.
Для начала установите в инспекторе объектов свойство формы ClientHeight и ClientWidth равным 512. Это для того, чтобы наша девушка влезла на форму полностью.
Теперь впишем в RenderEvent следущие строки и рассмотрим их подробно.

procedure TForm1.RenderEvent(Sender: TObject);
begin
  GameCanvas.UseImagePx(GameImages.Image[‘lena’], pxBounds4(0, 0, 512, 512));
  GameCanvas.TexMap(Point4(0,0 , 512,0 , 512,512,0,512), cAlpha4(255));
end;

   Эти две строки в итоге показывают Лену на форме. Как? Подробности ниже.
   Как видно все действия производятся с GameCanvas, то есть с поверхностью, куда выводится наше изображение!

UseImagePx(GameImages.Image[‘lena’], pxBounds4(0, 0, 512, 512)); — мы устанавливаем изображение Image[‘Lena’] для вывода. Мы берём полностью изображение pxBounds4(0, 0, 512, 512)) – у нас размер изображения 512×512.  Мы могли бы взять часть изображения меняя значения x1,y1,x2,y2 в pxBounds4. Кроме того мы бы могли взять сразу несколько одинаковых изображений, например 4, если укажем pxBounds4(0, 0, 1024,1024). Поэксперементируйте с этим, подставляя различные значения

GameCanvas.TexMap(Point4(0,0 , 512,0 , 512,512,0,512), cAlpha4(255)); —  выводим изображение используя координаты всех 4-ёх углов. Point4(x1,y1 , x2,y2 , x3,y3, x4,y4). Координаты идут в порядке часовой стрелки, смотрите рисунок. Такое обилие координат даёт большие возможности  – изображением можно крутить как угодно! UPD: Вместо Point4(0,0 , 512,0 , 512,512,0,512) в данном случае лучше использовать Pbounds4(0, 0, 512, 512), где первые две переменные — координаты левого верхнего угла изображения, а две последних переменных — ширина и высота изображения.
cAlpha4(255) – мы указываем значение альфаканала(прозрачность). 255 – это полностью непрозрачное изображение. 0 – это полностью прозрачное изображение(его не видно), а 128 – это 50% прозрачности. Тоже, на мой взгляд, может пригодиться в игрострое!

 TexMap – одна из самых главных(центральных) функций в Asphyre Sphinx, и к ней волей-неволей придётся неоднократно вернуться.
Запустите приложение и убедитесь, что поставленная задача выполнена. Изображение действительно отрисовывается на форме, заполняя её. Заметьте, при изменении размера Формы, изображение также меняется и по-прежнему заполняет всю форму. Это потому что мы используем единую координатную сетку DisplaySize – в будущем наш хороший друг!

   Наш пример можно скачать отсюда(1 мб вместе с картинкой и EXE). Далее он понадобиться как база для второго урока.

   Собственно, победа! Победа, ценой в месяц. Дело сдвинулось с мёртвой точки, а дальше будет всё значительно проще.
   Напомню, что уроки пишутся без знания Asphyre Sphinx с целью узнать эту технологию и помочь другим. Поэтому уроки могут быть слишком длинными и подробными, что, возможно, даже лучше.
   Следущий раз я постораюсь рассказать о некоторых дополнительных возможностях Asphyre Sphinx – может это будет работа с примитивами, а также манипуляции с изображением.

   Прошу все отзывы и вопросы писать мне на почту или на ветку Форума, мне интересны ваши мнения и замечания, а также предложения по грядущим урокам.

Перепечатка приветствуется, но обязательно с ссылкой на оригинал.

Илья aka RedMask

На урок 1 Урок 2 На урок 3

Leave a Reply

Comment moderation is enabled. Your comment may take some time to appear.