Последние записи
- Рандомное слайдшоу
- Событие для произвольной области внутри TImage
- Удаление папки с файлами
- Распечатка файла
- Преобразовать массив байт в вещественное число (single)
- TChromium (CEF3), сохранение изображений
- Как в Delphi XE обнулить таймер?
- Изменить цвет шрифта TextBox на форме
- Ресайз PNG без потери прозрачности
- Вывод на печать графического файла
Интенсив по Python: Работа с API и фреймворками 24-26 ИЮНЯ 2022. Знаете Python, но хотите расширить свои навыки?
Slurm подготовили для вас особенный продукт! Оставить заявку по ссылке - https://slurm.club/3MeqNEk
Online-курс Java с оплатой после трудоустройства. Каждый выпускник получает предложение о работе
И зарплату на 30% выше ожидаемой, подробнее на сайте академии, ссылка - ttps://clck.ru/fCrQw
29th
Июн
Детектор лжи с электрошокером
Детектор лжи с электрошокером будет служить отличным подарком любителям розыгрыша. Достаточно просто положить руку на детектор лжи и закрепить ее манжетой, которая входит в комплект. Задайте несколько вопросов испытуемому и в случае нечестного ответа электрошокер не сильно, но ощутимо даст знать, что вас пытались обмануть. Этот гаджет показывает уровень лжи на специальной шкале и имеет отличную память, поэтому отъявленного лжеца будет бить током все сильнее с каждым неправильным ответом.
Это заметка с четвертого выпуска журнала “ПРОграммист”.
Скачать этот выпуск можно по ссылке.
Ознакомиться со всеми номерами журнала.
26th
Июн
Как работать с графикой на канве в среде Дельфи. Урок 3-4
Одним из основных методов получения эффекта движущегося объекта является принцип уничтожения объекта на текущем месте и вывод его на новом с заданным приращением координат dX и dY. Однако прямолинейное применение такого метода на практике приводит к неприятному эффекту “мерцания” объекта во время движения. Причина этого в том, что при проведении этих операций непосредственно на канве объекта, где происходит движение, мы получаем двойную перерисовку участка канвы. Такая двойная смена цвета пикселей и является причиной. Сказанное наглядно видно на рисунке (см. рис.1), во время каждого такта движения происходит два процесса рисования: сначала старое изображение «затирается» фоном, а затем рисуется новое изображение движущегося объекта на новом месте…
Владимир Дегтярь
Урок 3
Создание движущихся объектов (еще немного теории)
Рис. 1. Демонстрация эффекта «мерцания»
Избежать данной проблемы можно, объединив обе операции «стирания» изображения и вывода нового в одну. Суть этого состоит в том, чтобы на канве объекта, невидимого на экране, вывести стирающий участок фона, затем вывести изображение движущегося объекта на новом месте и уже после этих операций вывести полученную комбинацию изображения на видимую часть канвы экрана. Для этого создаем дополнительный объект со свойством канвы (лучше всего подходит дополнительный буфер типа TBitMap), на котором и проводим предварительные операции. Размер такого дополнительного буфера определяется в зависимости от величины смещения движущегося объекта.
Возможны два варианта: когда старое и новое положение объекта имеют общую область (перекрывают друг друга) и не перекрывают (см. рис.2):
Рис. 2. Перекрытие объектов
Размер буфера в первом случае, как видно из рисунка 2 по каждой из координат равен размеру объекта плюс двойное приращение координат по соответствующим координатам (берется абсолютная величина). Теперь достаточно вывести в буфер область фона, соответствующего текущему положению объекта (старое положение) и наложить поверху изображение объекта, разместив его в буфере со смещением dX и dY (опять же по абсолютной величине) относительно нулевых координат буфера. После вывода изображения всего буфера на экран изображение объекта исчезнет и появится в новом положении.
Во втором случае, когда старое и новое изображения не имеют общей области также можно применить указанный метод. Но при этом, судя по рисунку 3, значительно увеличивается размер дополнительного буфера:
Рис. 3. Случай отсутствия перекрытия изображений
Можно уменьшить размер дополнительного буфера, применив немного другой подход (см. рис.4):
Рис. 4. Принцип уменьшения буфера
Здесь размер дополнительного буфера значительно меньше, чем на рисунке 3, но вывод изображения
объекта происходит не в центр буфера, а в один из четырех секторов. Перед заполнением буфера необходимо определить величину и знак следующего приращения dx и dy. Затем заполнить буфер фоном текущего положения объекта и вывести изображение объекта с учетом следующего приращения координат:
Если
dx > 0 и dy <= 0, то bx = dx и by = 0;
dx <= 0 и dy > 0 , то bx = 0 и by = dy;
dx <= 0 и dy <= 0, то bx = 0 и by = 0;
Значения bx и by присваиваются , естественно по абсолютным величинам dx и dy. Вот код программы для первого случая (см. листинг 1):
ЛИСТИНГ-1
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
procedure FormActivate(Sender: TObject);
procedure FormPaint(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
end;
var
Form1: TForm1;
{ битовые образы рисунка, фона и дополнит. буфера, соответственно }
Pic, Fon, Buf : TBitMap;
// области вывода фона
RectFon, RectBuf : TRect;
// текущие координаты
x,y: integer;
// приращения координат
dx, dy: integer;
// размеры дополнит.рисунка и буфера
W,H,WB,HB: integer;
implementation
{$R *.dfm}
procedure TForm1.FormActivate(Sender: TObject);
begin // инициализация TBitMap и TRect
// загружаем рисунок фона
Fon:= TBitMap.Create;
Fon.LoadFromFile(‘fon.bmp’);
{ загружаем рисунок объекта, который будет двигаться}
Pic:= TBitMap.Create;
Pic.LoadFromFile(‘picture.bmp’);
// задаем прозрачность объекту
Pic.Transparent:= true;
Pic.TransparentColor:= Pic.Canvas.Pixels[1,1];
{ определяем и устанавливаем размеры дополнит. буфера }
W:= Pic.Width;
H:= Pic.Height;
Buf:= TBitMap.Create;
Buf.Width:= W + 2*abs(dx);
Buf.Height:= H + 2*abs(dy);
WB:= Buf.Width;
HB:= Buf.Height;
// назначаем соответствие палитр цветов
Buf.Palette:= Fon.Palette;
Buf.Canvas.CopyMode:= cmSrcCopy;
{ определяем область в дополнит. Буфере для загрузки участка фона }
RectBuf:= Bounds(0,0,WB,HB);
// инициализация координат
x:=300; y:=150;
end;
procedure TForm1.FormPaint(Sender: TObject);
begin // выводим на экран фон и объект
Form1.Canvas.Draw(0,0,Fon);
Form1.Canvas.Draw(x,y,Pic);
end;
{ перемещение объекта на один шаг происходит после каждого нажатия
соответствующей клавиши}
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
case Key of
37: begin dx:= -3; dy:= 0; end;
38: begin dx:= 0; dy:= -3; end;
39: begin dx:= 3; dy:= 0; end;
40: begin dx:= 0; dy:= 3; end;
end;
{ определяем область фона, которую надо запомнить }
RectFon:= Bounds(x+dx,y+dy,WB,HB);
// запомним фон в буфере
Buf.Canvas.CopyRect(RectBuf,Fon.Canvas,RectFon);
// наложим в буфере на фон рисунок с прозрачным фоном
Buf.Canvas.Draw(abs(dx),abs(dy),Pic);
// выводим на форму в новой позиции
x:=x+dx; y:=y+dy;
Form1.Canvas.Draw(x,y,Buf);
end;
end.
А это для второго случая (см. листинг 2):
ЛИСТИНГ-2
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
procedure FormActivate(Sender: TObject);
procedure FormPaint(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
end;
var
Form1: TForm1;
{ битовые образы рисунка, фона и дополнит.буфера, соответственно }
Pic, Fon, Buf : TBitMap;
// области вывода фона
RectFon ,RectBuf : TRect;
// текущие координаты
x,y: integer;
// приращения координат
dx, dy: integer;
{ смещение изображения объекта
в дополнительном буфере }
bx, by: integer;
// размеры дополнит.рисунка и буфера
W,H,WB,HB: integer;
implementation
{$R *.dfm}
procedure TForm1.FormActivate(Sender: TObject);
begin // инициализация TBitMap и TRect
// загружаем рисунок фона
Fon:= TBitMap.Create;
Fon.LoadFromFile(‘fon.bmp’);
{ загружаем рисунок объекта, который будет двигаться}
Pic:= TBitMap.Create;
Pic.LoadFromFile(‘picture.bmp’);
// задаем прозрачность объекту
Pic.Transparent:= true;
Pic.TransparentColor:= Pic.Canvas.Pixels[1,1];
{ определяем и устанавливаем размеры дополнит. буфера }
W:= Pic.Width;
H:= Pic.Height;
Buf:= TBitMap.Create;
Buf.Width:= W + abs(dx);
Buf.Height:= H + abs(dy);
WB:= Buf.Width;
HB:= Buf.Height;
// назначаем соответствие палитр цветов
Buf.Palette:= Fon.Palette;
Buf.Canvas.CopyMode:= cmSrcCopy;
{ определяем область в дополнит. Буфере для загрузки участка фона }
RectBuf:= Bounds(0,0,WB,HB);
// инициализация координат
x:=300; y:=150;
end;
procedure TForm1.FormPaint(Sender: TObject);
begin // выводим на экран фон и объект
Form1.Canvas.Draw(0,0,Fon);
Form1.Canvas.Draw(x,y,Pic);
end;
{ перемещение объекта на один шаг происходит после каждого нажатия
соответствующей клавиши}
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
case Key of
37: begin dx:= -3; dy:= 0; end;
38: begin dx:= 0; dy:= -3; end;
39: begin dx:= 3; dy:= 0; end;
40: begin dx:= 0; dy:= 3; end;
end;
if (dx>0) and (dy>0) then begin bx:=abs(dx); by:=abs(dy); end;
if (dx>0) and (dy<=0) then begin bx:=abs(dx); by:=0; end;
if (dx<=0) and (dy>0) then begin bx:=0; by:=abs(dy); end;
if (dx<=0) and (dy<=0) then begin bx:=0; by:=0; end;
{ определяем область фона, которую
надо запомнить }
RectFon:= Bounds(x+dx,y+dy,WB,HB);
// запомним фон в буфере
Buf.Canvas.CopyRect(RectBuf,Fon.Canvas,RectFon);
// наложим в буфере на фон рисунок с прозрачным фоном
Buf.Canvas.Draw(bx,by,Pic);
// выводим на форму в новой позиции
x:=x+dx; y:=y+dy;
Form1.Canvas.Draw(x,y,Buf);
end;
end.
В принципе обе эти программы можно объединить, введя процедуру проверки на совпадение старой и новой областей изображения. Но это уже другая тема.
В данных примерах подразумевалось, что движущийся объект состоит всего из одного изображения ‘picture.bmp’. Но на практике, обычно таких изображений несколько. Наиболее часто изображения хранятся в файле в виде набора спрайтов. Вот здесь есть один небольшой подводный камень. Обычно выбор необходимого спрайта производится процедурой CopyRect() из буфера типа TBitMap, куда предварительно загружены изображения всех спрайтов. Но, при таком методе вывода изображения в дополнительный буфер с участком фона нельзя присвоить изображению свойство прозрачности.
Для обхода этого препятствия следует ввести еще один дополнительный буфер типа TBitMap, куда методом CopyRect() загрузить изображение нужного спрайта, а затем уже, присвоив свойство прозрачности, методом Canvas.Draw вывести изображение на канву дополнительного буфера с участком фона.
Следует отметить, что для создания «эффекта» движения можно оставить изображение графического объекта неподвижным, а двигать сам фон. При этом размер фона должен быть больше размера окна формы. Также можно применять одновременно или поочередно сдвиг объекта и сдвиг фона.
Урок 4
Проект с движением звездолета.
Аналогично Уроку 2 создадим новый проект и сохраним его под именем Lesson2. Параметры формы Form1 также установим аналогично Уроку 2, изменив только заголовок Form1 на «Урок по графике № 2».
Для получения движения объекта применим метод, когда сам объект (звездолет) неподвижен на форме, а “двигается” сам фон. Фон создадим в два раза больше по высоте размера окна формы. Для этого используем файлы ‘star1.bmp’ и ‘star2.bmp’. Нам также понадобится еще один дополнительный буфер BufFonDop. Кроме этого введем еще переменные – координаты вывода на форму фона и звездолета, а также приращения к координатам (см. листинг 3 ):
ЛИСТИНГ-3
Form1: TForm1;
BufFon,BufFonDop,BufSpr,BufPic,Buffer: TBitMap;
xS1,yS1: integer; // координаты звездолета ‘ship1’
xf,yf: integer; // координаты вывода общего буфера на форму
dyf: integer = 2; // приращение изменения координаты yf по вертикали
dxS1,dyS1: integer; // приращение координат звездолета по гориз. и вертик.
ns: byte; // номер спрайта
{Здесь-же вводим начальные данные для переменных}
xs1:= 250; ys1:= 1006; // начальные значения переменных-
dxs1:= 4; dys1:= 2;
xf:= 0; yf:= -540; dyf:= 2;
{ Инициализацию буферов проведем в процедуре OnCreate() формы }
procedure TForm1.FormCreate(Sender: TObject);
begin
BufFon:= TBitMap.Create;
BufFonDop:= TBitMap.Create;
BufSpr:= TBitMap.Create;
BufPic:= TBitMap.Create;
Buffer:= TBitMap.Create;
BufFonDop.LoadFromFile(‘data/star1.bmp’); // загрузка 1-го рисунка фона из файла
BufSpr.LoadFromFile(‘data/ship1.bmp’); // загрузка спрайтов из файла
BufFon.Width:= BufFonDop.Width; // размеры общего буфера фона
BufFon.Height:= (BufFonDop.Height) * 2; { по высоте = двум Height одного
рисунка фона}
BufFon.Canvas.Draw(0,0,BufFonDop); // заносим 1-й рисунок фона
BufFonDop.LoadFromFile(‘data/star2.bmp’); // загрузка 2-го рисунка фона из файла
BufFon.Canvas.Draw(0,540,BufFonDop); // заносим 2-й рисунок фона
Buffer.Width:= BufFon.Width; // размеры общего буфера = размерам буфера фона
Buffer.Height:= BufFon.Height;
BufPic.Width:= round((BufSpr.Width) / 2); // размеры буфера рисунка одного спрайта
BufPic.Height:= BufSpr.Height;
В процедуре DrawShip1(i) добавлено условие прозрачности фона рисунка звездолета (см. листинг 4):
ЛИСТИНГ-4
begin
BufPic.Canvas.CopyRect(bounds(0, 0, BufPic.Width, BufPic.Height),
BufSpr.Canvas,bounds( i * 66, 0, BufPic.Width,
BufPic.Height));
BufPic.transparent:= true;
BufPic.transparentcolor:= BufPic.canvas.pixels[1, 1]
end;
«Эффект» движения организован в обработчике таймера. В каждом такте таймера 50 мсек:
- смещаем координату вывода общего буфера yf на dyf вниз (+ 2)
- координату yS1 вывода звездолета смещаем на dyS1 вверх (- 2). В этом случае изображение звездолета будет неизменным в координатах формы, а двигаться будет только фон
- в общий буфер (Buffer) выводим фон (при этом «уничтожается» изображение звездолета на старом месте)
- выводим в общий буфер на фон новое изображение звездолета со смещением dyS1
- выводим общий буфер на форму
В процедуре DrawShip1(ns) через переменную ns, принимающую попеременно значения 0 или 1, выводятся поочередно спрайты изображения звездолета (см. листинг 5):
ЛИСТИНГ-5
begin
ns:= ns + 1; if ns > 1 then ns:= 0; // смена спрайтов
DrawShip1(ns);
yf:= yf + dyf; if yf > 0 then begin yf:= -540; yS1:= 1006; end; // приращение yf
yS1:= yS1 — dyS1; // приращениие звездолета обратно приращению фона
Buffer.Canvas.Draw(0,0,BufFon); // выводим фон в общий буфер
Buffer.Canvas.Draw(xS1,yS1,BufPic); // выводим рисунок спрайта поверх фона
Form1.Canvas.Draw(xf,yf,Buffer); // выводим все на форму
end;
//Движение звездолета по горизонтали в пределах окна формы организовано
//в обработчике нажатия клавиши OnKeyDown
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
case Key of
37: begin // клавиша "стрелка" — влево
if xS1 <= 0 then EXIT; { если звездолет у левого края формы
xS1 не изменяется}
xS1:= xS1 — dxS1; // движение звездолета влево
end;
39: begin // клавиша "стрелка" — вправо
if xS1 >= 630 then EXIT; { если звездолет у правого края формы
xS1 не изменяется}
xS1:= xS1 + dxS1; // движение звездолета вправо
end;
end;
end;
Запустим проект на компиляцию и взглянем на результаты нашего труда (см. рис.5):
Рис. 5. Движение звездолета по-горизонтали
Заключение:
В следующих уроках добавим еще несколько движущихся объектов в проект.
Рассматриваемые в данной статье проекты полностью приведены в ресурсах к статье на http://www.programmersforum.ru в разделе «Журнал клуба программистов. Второй выпуск» (папка Lesson2*).
Обсудить на форуме – Как работать с графикой на канве в среде Дельфи. Урок 3-4
Статья из второго выпуска журнала «ПРОграммист».
24th
Июн
Гимн Программистов
Хорошая идею подал форумчанин _PROGRAMM_ : написать гимн программистов. И его быстро и весело поддержали на форуме.
На данный момент имеем:
Программисты – умный народ
И пляшет и гимны поет
Он очень весело живет
И так же весело жует
А бывает с бубном ходит
И тоску на всех наводит
Он не спит ни день, ни ночь,
Всем мечтает он помочь
Утром сядет у окна
А чуть позже у компа
Поиграет чуть в ФИФА
И давай дразнить кота
Но когда пришла зима,
Пиво кончилось тогда
Не успев никто моргнуть
В двор примчался старый друг
И позвал его гулять,
Праздник лета отмечать
Раз бокал и два бокал
И давай писать в барланд!
Это еще не релиз, это скорее всего “ночная сборка”. Возможно, в скором будущем мы создадим гимн, наложем на нег музыку, запишем хит и будем продавать на лицензионных DVD .
24th
Рассылка. Выпуск 64.
От ведущего рассылки.
Здравствуйте. Новый выпуск рассылки от клуба программистов. Сегодня подборка самых интересны тем за неделю с форума программистов.
24th
Определение разрядности Windows
1. Средствами самой ОС.
1) ХР.
Пуск – Выполнить.
Введите команду winmsd.exe и нажмите ОК.
2) Vista
Пуск. Введите система в поле Начать поиск и затем щелкните пункт Сведения о системе в списке Программы.
Если в панели навигации выбран пункт Сводные сведения о системе, то сведения об операционной системе отображаются указанным ниже образом.
* Для 64-разрядной версии операционной системы. Фраза Компьютер на базе x64 отображается в пункте Тип системы в разделе Элемент.
* Для 32-разрядной версии операционной системы. Фраза Компьютер на базе x86 отображается в пункте Тип системы в разделе Элемент.
2. Самодельная программа и исходник.
23rd
Июн
Персональный язык программирования
Статья имеет целью познакомить читателя с одним представителем огромной армии неизвестных языков программирования. Этот язык я придумал сам для себя, он ни на какой язык не похож и этим может быть интересен.
Виктор Кон
by http://kohnvict.narod.ru
Про языки программирования написано очень много статей и философских размышлений. Их много и есть необходимость в их классификации. Главный аспект – это то, что языки могут быть многоуровневыми. Есть ассемблер, есть базовые языки более высокого уровня: Basic, C++, Java и много других. С их помощью можно написать компиляторы и интерпретаторы других языков еще более высокого уровня и так далее. Второй аспект – разделение языков на компилируемые и интерпретируемые. Первые используются для создания независимых и конечных программ, например программы решения квадратного уравнения. Вторые являются текстовыми инструкциями для одной единственной программы – интерпретатора. Бывают и пересечения, когда программа частично компилируется в промежуточный код, который затем интерпретируется. Ну и, наконец, третий аспект – структура самой программы, тут три уровня. Вся программа одним куском, программа с процедурами, программа с объектами. Какой язык и какая программа удобнее для конкретного пользователя зависит от свойств самого пользователя.
Вместо введения
Я хочу рассказать о себе. Я пользователь в том смысле, что пишу программы для своей научной работы. Почти все программы моделируют те или другие физические процессы и часто используются только один раз. Условно конечно, много раз в процессе отладки до тех пор пока не получен достоверный результат. После того, как результат получен – программа уже не нужна. Опять неточно, нужен ее код, чтобы развивать его дальше и учесть более сложные процессы, но на каждом этапе программа пишется на результат, который получается конечным числом ее запусков. Как писать такие программы? Можно конечно одним куском и на ассемблере. Но это будет огромный труд с очень низким коэффициентом полезного действия. Можно писать процедуры и компилировать их в новые сборки программ. Но и это не оптимально. В каждой программе будет очень много одного и того же кода, который будет повторяться, засоряя носители информации. Это не так важно сейчас, но было важно раньше, когда компьютеры были маломощные. Сама практика подсказала, что наиболее оптимальным путем является программа-интерпретатор, выполняющая команды одну за другой, а наиболее удобным и простым для выполнения конкретных работ является командный язык программирования с возможностью группировать код в процедуры.
Так получилось, что я пришел к этому выводу в далеком 1991-м году, когда программ еще было мало, все работали в операционной системе ДОС, которая мало что умела и о программах типа Mathematica, IGOR-pro и им аналогичных даже и не мечтали. Моя идея состояла в том, что нужно сделать интерпретатор языка программирования высокого уровня, который имел бы команды всех уровней, начиная от прерываний ассемблера и кончая запуском редактора текстов или программы построения графика одной командой. В то же время структура языка должна быть такой, чтобы четкие и конечные операции можно было сделать написав несколько строк текста. Я потратил два года работы и сделал себе такую программу. Работая в ДОС, она имела графические возможности, необычный редактор текстов с уникальными возможностями, хорошие возможности по интерфейсу и очень мне помогала в работе. Самый главный плюс такой программы состоял в том, что я мог очень быстро получить ответ на многие сложные вопросы. А быстрота получения ответа иногда имеет решающее значение. Часто просто нет времени на написание и отладку огромного кода, ответ нужен, как говорят, еще вчера. И я мог это делать.
Минусы у программы тоже были – отсутствие шрифтов. Конечно, я создал свой масштабируемый фонт, который можно было использовать, но это было не очень быстро и не очень красиво. Когда появилась Виндовс, а компьютеры стали более мощными, необходимость в интерпретаторе отпала. Самый главный минус был в том, что программа могла работать только в ДОС. Виндовс не разрешал многие операции, которые она могла делать, да и просто ее не запускал по соображениям безопасности. Я снова рассыпал программу на отдельные куски, но и они постепенно становились все менее привлекательными.
Прошло время – появились КПК (карманные персональные компютеры), это чудо на ладони с большими возможностями по части поиграть музыку или показать кино. Но они не умели делать вычисления, не было программ. А уж о том, чтобы программировать прямо на КПК не было и речи. Я снова вспомнил про свой язык программирования и свой интерпретатор. Код интерпретатора был написан на фортране, но для КПК нужно было писать на чем-то другом. Так получилось, что я в то время (это уже был 2004 год) начал работать на Java, поэтому выбор был сделан. Я просто перетранслировал вручную весь код с фортрана на Java, даже порой уже не понимая, что этот код конкретно делает. И все получилось – код заработал. Конечно, пришлось отказаться от прерываний, немного по-другому написать редактор текстов, по-другому сделать интерфейс. Но сам язык программирования и его интерпретация почти не изменились. Эта программа до сих пор работает у меня на КПК, который я как раз и купил в начале 2004 года. Он до сих пор работает с той же самой батареей. Это отдельная тема и я не буду ее развивать здесь.
Важно, что мне снова понравилось работать на своем языке, и я сделал клон этой программы также и для ПК, то есть настольного компьютера и ноутбука. Эта программа имеет больше возможностей и настолько удобна, по крайней мере, для меня, что я уже не мыслю себе другой жизни. Вот вчера мне понадобилось выделить из pdf файла книги в 150 страниц всего 4 страницы и записать в другой pdf файл. На моем яыке это делается в несколько строк, но и их писать не надо – они уже написаны. А вычислить, скажем, таблицу значений функции Бесселя и построить график можно с помощью программы в одну строчку.
Но это только примеры. Реально программа умеет очень много всего, практически все, что лично мне нужно и самое главное – если чего-то нехватает, то просто дописываем интерпретатор и создаем новую команду. Сложность только в том, что, как и у любого интерпретируемого языка, команд очень много. Но они хорошо структурированы и есть описание с хорошей навигацией. Так что перечитав описание можно все легко вспомнить. Наизусть я свой язык не помню.
Вместо рекламы
Цель этой статьи – просто познакомить уважаемую публику с тем, что у меня есть. С самого начала я не стремился делать коммерческий продукт, но в какой-то момент работы над программой я подготовил вариант, который содержит только команды общего назначения, могущие представлять интерес для всех. Этот вариант я выложил в интернет на отдельный сайт http://vkacl.narod.ru, а также разместил в каталоги программ. Неожиданно программа стала популярной на сайте FreeSoft, где ее скачали уже более 40 тыс. раз. Были и письма, из которых я понял, что кое-кто действительно научился пользоваться. У программы есть несколько вариантов. Первый, основной, содержит интерпретатор языка, который я назвал ACL (advanced command language), среду разработки, включающую встроенное описание языка, и примеры готовых программ. Эта программа называется vkACL.jar. Как любая Java программа, она сама запускается через виртуальную машину Java или JRE, которую предварительно надо установить на компьютер. Дистрибутив JRE можно бесплатно скачать хоть с родного сайта, хоть с разных зеркальных сайтов, например [1]. Скриншот программы можно посмотреть вот по этому адресу [2].
Есть также вариант проигрывателя с окном, то есть это программа, которую можно настроить таким образом, что она будет показывать меню и выполнять (интерпретировать) одну ACL программу. Такой вариант не содержит помощи и выглядит как конечная программа на одну функцию. При этом jar-файл может иметь любое имя. Программу в таком варианте я использую для разработки конкретных программ, с которыми могли бы работать мои коллеги, не умеющие программировать. Эти программы выглядят как обычные Java программы. Есть еще и второй вариант проигрывателя, который совсем не имеет головного окна. Такая программа либо выполняет свою работу, не общаясь с пользователем совсем (так работали старые программы в эпоху до ПК), либо средства общения с пользователем написаны в самой ACL программе. Иногда такой вариант бывает оптимальным.
Наконец, есть вариант программы в виде Java-апплета, который сразу работает в интернете и запускается вот по этому адресу [3]. Этот вариант сделан совсем недавно и я его сейчас развиваю. Если у кого есть быстрый интернет, то можно сразу пользоваться программой с любого компьютера. К сожалению Java-апплет не может работать с файлами пользователя, но я организовал ввод и вывод данных через редактор текстов. Любую информацию, в том числе и картинки, можно ввести в программу и вывести из программы копированием текста через буфер обмена (клавиши Ctrl-A, Ctrl-C, Ctrl-V), а готовые картинки можно скопировать с экрана, используя любую программу с функцией snapshot, то есть фотографирования части экрана в файл. Таких программ много, подробности можно прочитать в моей статье http://kohnvict.narod.ru/Data/advice.htm. Java — апплет содержит несколько готовых программ общего назначения, в частности таблицу Менделеева в виде книги, а также мои научные программы для коллег. Также каждый может скопировать и выполнить свою программу.
Что такое ACL?
Невозможно в двух словах объяснить, как работает космический корабль, слишком там много деталей. Такая же проблема с языком программирования, который рассчитан на то, чтобы делать все на свете. Когда я начинал, то не имел никакого опыта, да тогда вообще все было в очень ограниченном ассортименте. Поэтому я ничего не читал, а все выдумал сам. Первый момент – никаких строк и меток. Символ конца строки ничего не значит. В эпоху ДОС это было революционно. Программа пишется таким образом, что как раз комментарии ничем не выделяются, они пишутся свободно, а вот сами команды языка программирования начинаются с символа (#). Этот символ для интерпретатора служит знаком, что пора начинать работу. После этого знака пишется имя команды. Принцип написания всех имен следующий: можно писать сколько угодно букв, но слитно (без пробелов). Реально команды различаются максимум по трех первым буквам, а часто даже просто по одной букве. Я не люблю много писать, поэтому мне не нравится даже Java, хотя я считаю ее лучшим языком программирования. Команды из одной или двух букв абстрактны, но это дело привычки. Любой язык хорош, который знаешь. Вот у меня фамилия Кон. И это намного удобнее, чем Константинополев. Итак, читается слово до пробела и выбирается три первые буквы или меньше, если их меньше.
Что дальше?
Каждая команда – это отдельный блок программы, который знает, умеет и выполняет свою работу. Но часто ему нужны данные, конкретизирующие задачу. Входные данные раделены на два типа. Первый тип – это целочисленные параметры, которые имеют имена. Сделано это исключительно для более удобного понимания программы. Параметры универсальные и используются разными командами, причем один раз определенный параметр сохраняет свое значение до следующего определения. Определять параметры можно после каждой команды в поле, заключенном в квадратные скобки. Есть дополнительно к целочисленным несколько текстовых параметров. А всю остальную информацию надо записывать как аргументы сразу после квадратных скобок. Аргументы задаются как во многих командах многих интерпретаторов, хоть в ДОС (батч- файлы), хоть в компиляторах и прочих программах с командной строкой. Потому язык и называется командным.
Самое интересное, что больше ничего и нет. Точнее нет грамматики языка. Одни команды. И вся грамматика тоже реализована через команды. Одна команда запускает огромную готовую программу, другая реализует грамматику, но структурно они неразличимы. Возникает законный вопрос – а как делать вычисления? А где циклы, условия, логика? Ведь языки программирования, как и разговорные языки – как ни пиши, а предмет то один и тот же. Все верно. Но у меня «быстрый» язык, он кстати в первой редакции назывался quick. Для него главное – запускать готовые блоки, а все остальное вспомогательное. Поэтому все остальное реализовано по минимуму. Переменные все реальные и их имена короткие – одной буквой от (a) до (z), затем от (A) до (Z) и еще три символа ($), (%). Можно писать одну букву и одну цифру, например (z1=x3;). И все – больше переменных нет, а зачем много? Массивы – есть один целый массив i(), один реальный массив r() и один массив символов (уникодов) t(). Индексы в круглых скобках. Каждый из массивов имеет фиксированный (большой) размер, а все команды работают с частями этих массивов, причем есть специальные параметры (b [begin]) и (le [length]), которые как раз и выделяют индексы массивов, используемые в программе. То есть фактически вместо имени массива используется его начальный индекс в большом массиве. А зачем имена? Проблема выбора для некоторых людей – непреодолимое препятствие. А так все массивы плотно и динамически упаковываются в одном. Нет строк. Массив символов и строка – одно и то же. Более того, символы и числа – тоже одно и то же. Элементы символьного массива могут участвовать в вычислениях.
Как делаются вычисления? А очень просто. Открывается команда вычислений #cal или #c или просто #. Это единственная команда, которая может иметь нулевое имя. И все формулы вычислений являются ее аргументами. Она вычисляет и переопределяет переменные и массивы, это ее работа. В вычислениях можно использовать имена функций, кроме стандартных есть еще функции Бессела и интегралы Френеля. Пример: # z=sin(3.14*20/180); X1=5/exp(-2.7)+r(25); #% А что с циклами? Есть простой цикл, релизованный командой #rep N (repeat) с одним аргументом (числом повторений). Все команды после этой выполняются до команды #end , которая возвращает сканирование программы в самую первую команду после #rep и так ровно N раз. Все индексы и переменные должны переопределяться внутри цикла вручную. А условные циклы? Вот тут интереснее. Все сделано с помощью всего одной команды – и условия, и условные циклы. Зачем много? Опять проклятая проблема выбора. Просто есть еще одна команда цикла, она называется #case N.
Эта команда так же точно, выполняет все команды до команды #end. Но при одном условии, что ее аргумент совпадает со значением переменной (&). Если не совпадает, то вообще ничего не делается. В данном случае команда #end снова проверяет это же самое условие. Если не изменить переменную (&), то цикл будет делаться бесконечно. Поэтому внутри тела цикла ее надо изменить как раз тогда, когда надо. Таким способом можно сделать выбор: # &=2; #case 1 . . . #end | #case 2 . . . #end | #case 3 . . . #end | Вот в этом коде выполнится только многоточие после #case 2. Знак вертикальной черты (|) после #end через пробел означает автоматическую смену переменной (&) на значение 12345. Таким образом, тело цикла выполнится один раз. Перебор вариантов мы делаем. Но нам нужна логика. Программа должна уметь мыслить.
На это я могу авторитетно заявить. Не умеет компьютер мыслить. Все что он умеет – это сравнивать два числа на больше и меньше, а еще точнее – определять знак числа. И одной этой операции ему хватает, чтобы обыгрывать человека в шахматы. И не только в шахматы, а вообще делать разумную работу. Я не стал напрягаться и делать как у всех. Я просто ввел две новые операции < и > в математические вычисления. Результатом операции a<b является минимум из двух значений a и b. Аналогично a>b вычисляет максимум. И все, больше ничего не надо. Это позволяет писать полноценные программы, реализующие любую логику при переборе вариантов.
Заключение
Есть еще много интересных нюансов в языке и много такого, что позволяет ему иметь неограниченные возможности по созданию очень большой программы, включающей разные куски кода, написанные в разных файлах где угодно, в том числе и на серверах в интернете. Есть готовая научная графика, возможность работы с картинками, zip- архивами, делать любые операции с файлами и текстом. Но об этом можно очень долго писать. Моя задача состояла в том, чтобы заинтересовать читателя обратиться на указанные выше сайты за более подробной информацией. Я надеюсь, что эту задачу я выполнил. Напоследок скажу чего нет. Нет работы с базами данных стандартного вида, хотя аппарат работы с небольшими базами данных есть. Я не работаю с базами данных и мне это не нужно. До сих пор я не освоил аппарат работы с xml файлами, каюсь. Но и это мне не нужно. Персональный язык программирования не обязан быть универсальным.
Ресурсы
- Дистрибутив JRE http://java.sun.com или http://www.java.com/ru/download/index.jsp
- Скриншот программы http://img-fotki.yandex.ru/get/3904/kohnvict.12/0_24d12_9b5a6cf9_orig
- Вариант программы в виде Java-апплета http://vkacl.narod.ru/applets/00/vkACLa.htm
Обсудить на форуме – Персональный язык программирования
Статья из первого выпуска журнала «ПРОграммист».
23rd
GAMEDEV на Delphi. Делаем змейку
Вэтой статье я подробно рассмотрю создание игры «Змейка» с использованием ООП (объектно-ориентированного программирования). Делать игру мы будем средствами GDI на Delphi 7 (хотя должна подойти и любая другая версия). Для полного осознания того, о чем говорится в статье желательно иметь хоть какой-нибудь опыт программирования, знать что такое Сanvas и TBitmap, что такое классы и в чем заключаются их особенности.
Вадим Буренков
Вообще, при создании любой игры можно выделить три момента:
- Создание идеи, примерный план реализации. При желании разработчиком пишется «диздок» (дизайн-документ — это документ, который описывает то, что вы хотите создать).
- Алгоритмизация. То есть мы представляем план строения игры, какие типы данных будут использоваться. Обдумываются алгоритмы работы и взаимодействия элементов игры.
- Все выше мы записываем в виде кода, то есть программируем.
В принципе, игра не использует наследования, поэтому можно было обойтись и без классов, но мне захотелось сделать так.
Краткий экскурс…
При создании любой игры нужно придерживаться определенного строения кода.
Каждая игра имеет следующие секции:
- инициализация (тут происходит настройка окна для рендера (вывода изображения), загрузка всех игровых ресурсов и уровня)
- обработка (всех игровых объектов, их взаимодействие друг с другом. Эта секция в отличие от инициализации и деинициализации выполняется постоянно, причем скорость выполнения измеряется в миллисекундах и обычно** равна 10-100 мс)
- деинициализация (выполняется при завершении работы приложения и служит для очищения занятой приложением памяти)
Обратите внимание! Компонент TTimer имеет низкую точность. Ставить на нем низкие значения (менее 30 мс) не имеет смысла.
Сделаем основу для игры
Откройте Delphi, создайте новое приложение. Сразу хочу заметить, что именам всех переменных, окон, файлов, и.т.п. надо давать осмысленные имена. Конечно, кто-то скажет что и так запомнит что такое Form1 и Label2, но когда пишется проект не на 10 а на 10000 строк и используется не одна, а пятнадцать форм… Также не надо мелочиться на комментарии, поскольку смысл определенного кода через месяц простоя не поймет и тот, кто его написал, не говоря уже о том – что вам может понадобиться сторонняя помощь.
Назовем форму MainForm, модуль GameMain, а проект SnakeProject. Поскольку эта игра простая и небольшая, то я буду использовать один модуль, но в остальных случаях рекомендуется использовать свой модуль для отдельной игровой части. Создадим два события формы: OnCreate и OnDestroy которые и будут инициализатором и деинициализатором игры. За обработку игры будет отвечать компонент-таймер TTimer (поместите его на форму из вкладки System, назовем его MainTimer), а именно его событие – OnTimer.
Я не буду делать игру очень гибкой в плане игрового поля (его размеры нельзя будет менять. Поле будет 16х12 клеток, но о них будет написано ниже, основные параметры будут константами. Игровое же поле 640х480 по центру экрана без возможности перемещения (без синей рамки). Объявим константы:
SCR_WIDTH=640;
SCR_HEIGHT=480;
Можно не выставлять параметры окна в среде разработки, а написать их в коде (событие OnCreate):
MainForm.Width := SCR_WIDTH;
MainForm.Height:= SCR_HEIGHT;
// параметры формы
MainForm.BorderStyle:= bsNone; // без рамки
MainForm.Position := poDesktopCenter; // по центру экрана
Теперь заставим работать таймер (изначально параметр таймера должен быть enable = false):
MainTimer.Enabled:= true;
Создадим и добавим в событие OnTimer две процедуры (пока пустые) – Main_Update() и Main_Draw(). Для чего это? Мы ведь можем писать весь код напрямую в таймер. Поясню. Например, если мы захотим сделать в игре меню, то должны быть отдельные процедуры обработки/рисования для меню и игры и выполняться они будут в зависимости от того, где игрок находится:
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() напишем код отрисовки буфера (все рисование должно происходить до этих строчек):
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. Зная координату клетки можно получить координату на экране, умножив ее на размер клетки. Это нам понадобится при рисовании****. Эти вспомогательные функции делают следующие:
begin
result:= x*CAGE_SIZE
end;
Function G_Y(Y:integer):integer;
begin
result:= Y*CAGE_SIZE
end;
Программируем поведение змейки
Обдумаем, как же будет работать змейка. Каждая деталь змейки – это отдельный элемент со своими координатами. Когда змейка движется – первый элемент (голова) занимает новую позицию, а второй элемент занимает позицию предыдущего до перемещения (там, где раньше была голова), третий позицию второго и.т.д. Это самый логичный вариант, который приходит в голову. Но тогда надо сохранять координаты, где было тело змейки. Для упрощения кода есть более легкий способ – двигать змейку с хвоста. Перейдем к созданию класса змейки:
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» облегчает нам задачу хранения координат и взаимодействия с ним:
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():
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, которые не позволяют ей этого сделать:
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;
И две последние процедуры. Отрисовка отобразит все части змейки по координатам (голова отрисовывается два раза для наглядности):
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. Отображение нашей змейки
Объекты в игре. Не дадим змейке «помереть с голоду»
Теперь необходимо добавить объекты. Они будут двух типов:
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;
Тут ничего сложного нет. Многие процедуры аналогичны змейке:
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(). Вот полный код инициализации змейки и стен. Объявим следующие переменные:
Wall: array of TGameObject; // все стены игры
WallNum:integer; // их кол-во
Food:TGameObject; // еда для змейки
И добавим в инициализацию следующий код:
MySnake:= TSnake.Create(‘snake.bmp’,v2(7,7),2);
// теперь создам еду. А на ужин у нас аппетитненький пингвинчик
Food:=TGameObject.Create(‘food.bmp’,oFood,RandomFieldPos);
Чтобы не вводить координату каждой стенки уровня, так как загрузчика уровней нет, я просчитал создание всех стен в операторе for:
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() для инициализации генератора случайных чисел:
begin
result:=v2(random(13)+1,random(9)+1); // я просчитал допустимые значения
// по X от 1 до 14, по Y от 1 до 10
// тут ничего сложного нет.
end;
Собираем запчасти
Теперь надо добавить нашу змейку, стены и пингвинчика в обработку и отрисовку. Поскольку скорость движения змейки надо ограничить, мы заводим переменную-счетчик WaitTime. Она считает до 5 и выполняет процедуру движения и сбрасывает себя на 0. В итоге, MySnake.Update() срабатывает в 5 раз реже таймера. Ошибкой многих начинающих разработчиков является использование большого количества таймеров, что сильно усложняет код. Чтобы из игры можно было нормально выйти, сделаем условие нажатия на клавишу <ESCAPE>:
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;
Напишем «правильное очищение» всех ресурсов игры:
var i: integer;
begin
scr_Buffer.Free;
BackImage.Free;
MySnake.Free;
for i:=0 to wallnum-1 do wall[i].Free
end;
После чего остается процедура «проигрыша» 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. Делаем змейку
Статья из второго выпуска журнала «ПРОграммист».
23rd
Создание спектрограммы в Дельфи
Многие начинающие программисты пробуют свои силы в создании аудиоплеера. И вот когда у них более-менее получается создать функциональную основную часть и музыка уже играет, хочется добавить еще чего-нибудь крутого. Часто это спектроанализатор или спектрограмма, которую многие ошибочно называют эквалайзером (эквалайзер – это средство настройки, а не анализа звука) [1].
Александр Терлецкий
В этом уроке мы попробуем создать спектрограмму на Delphi, а прикручивать ее мы будем к движку BASS. Скажу сразу, что существует Delphi оболочка для BASS от нашего корейского коллеги под названием TBassPlayer, в которой есть спектрограмма, но наша задача – научиться самим, а не использовать готовые решения. Однако вам никто не запрещает просмотреть исходный код этого компонента (он свободный), это никогда не повредит. Изучив этот урок, вы научитесь создавать свою собственную, уникальную своим внешним видом спектрограмму. Желательно, чтобы вы имели основные понятия об ООП, так как я не буду подробно останавливаться на этом и больше внимания уделю вопросам вывода графики.
Урок разбит на две части. В первой – пишем класс спектрограммы и отлаживаем его, во второй – прикручиваем его к звуковому движку BASS (вы можете использовать и другой движок, FMOD например, но вам придется разобраться самостоятельно как получить уровни частот). Вариант с самостоятельным разбиением спектра с помощью преобразования Фурье, без использования каких либо звуковых библиотек, я не рассматриваю вообще, если кому очень интересно попробовать, ищите информацию в Интернете, он большой и там все есть*.
Часть 1. Создание класса спектрограммы
Листинг программы смотрите в приложении к журналу [12]. Создадим новый модуль для нашего класса, назовем его Spectrum. В нем описываем главный класс спектрограммы TAnalizer, и вспомагательные TChannel и TChannels для каналов частот. Класс TAnalizer мы наследуем от TPaintBox и добавляем к нему несколько нужных нам полей, это:
- Timer (нужен для анимации ниспадающих пиков, в принципе можно было бы обойтись и без собственного таймера, но так наш анализатор получает большую независимость от деталей реализации использующего его приложения)
- Buffer: TBitmap (буфер нужен чтобы не было мерцания при обновлении изображения)
- Channels (массив каналов, кол-во их будет настраиваемое)
Сами каналы у нас будут иметь градиентную заливку, которая задается полями LowColor: TColor; и HighColor: TColor.
Количество каналов и их координаты на экране задается в методе TChannels.SetCount. Для обратной связи с хозяином в классе TChannels предусмотрено поле Owner, которое заполняется сразу после его создания в конструкторе класса TSpectrum. Возможно, здесь я отошел от канонов. Обычно поле Owner в VCL используется немного по-другому, но в данном случае мне было удобнее сделать так и вообще я не претендую на академичность.
Подробно остановлюсь на методе Draw. В нем происходит обновление изображения в такой последовательности. Очищается буфер и заливается черным фоном, затем с помощью Win API функции GradientFill() мы заливаем градиентом все прямоугольники каналов. Функция GradientFill() довольно сложная и имеет много параметров, так что перед ее вызовом много строк кода занимает заполнение нужных структур для передачи в нее. Затем мы проверяем, есть ли пики, которые стали ниже уровня соответствующей им частоты с момента последней отрисовки, если таковые есть, обновляем их положение. Те пики, которые наоборот стали выше текущего уровня, мы пока не трогаем, они будут обработаны в таймере, чтобы они плавно падали вниз (но рисуются все они в этой процедуре, в таймере только смена позиций и вызов отрисовки). После этого мы закрасим черным (цветом фона) верхушки тех каналов, уровни которых ниже максимального. БылаТут я использованал функцияю из WinAPI – OffsetRect() для смещения прямоугольника канала вверх, иа затем, после закрашивания, повторный вызов OffsetRect(), только теперь вниз, для восстановления прежних координат прямоугольника канала. И в завершении рисуем пики (короткие горизонтальные линии) для всех каналов. После этого у нас имеется обновленное изображение спектрограммы в буфере, и мы выводим его на экран, а точнее на канву нашего объекта.
Чтобы изображение не пропадало с нашего компонента при перерисовке окна, мы обрабатываем унаследованный метод Paint и в ней вызываем нашу процедуру обновления Draw(). Хотя с тех пор, как я добавил таймер, это уже лишнее, можете убрать обработку Paint() если хотите, но если нет таймера, она необходима.
Класс написан, пора его протестировать. Для этого создадим простое приложение. Для чего, поместим на форму TGroupBox, чтобы удобнее было размещать наш анализатор, и две кнопки, по нажатию одной – будет задаваться случайный уровень для всех каналов (см. рисунок 1):
Рис. 1. Тестовая форма. Спектрограмма случайных величин
По нажатию другой – пройдет волна (см. рисунок 2):
Рис. 2. Тестовая форма. Спектрограмма волновой функции
Для волны нам понадобится таймер и подключение в Uses модуля Math, так как мы будем использовать синус. По нажатию третьей – все каналы по очереди заполнятся пиковым значением. Визуально это будет, похоже, как будто кто-то пальцем провел по клавишам пианино (см. рисунок 3):
Рис. 3. Тестовая форма. Спектрограмма в виде пилы
Смотрите листинг, компилируйте ,и разбирайтесь. Если все понятно, то переходим к части 2, игде будем подключать анализатор к плееру.
Часть 2. Подключение к BASS
Как это осуществить? Здесь я отдельно рассмотрю два случая, подключение к компоненту-оболочке TBassPlayer и непосредственно к BASS. Ну что-ж, приступим…
TBassPlayer
Хочу сразу обратить ваше внимание на нюансы с различными версиями как Delphi, так и TBassPlayer. Как вы знаете, начиная с D2009, для строк используется Unicode. Если у вас Delphi более ранних версий, можете дальше не читать и переходить к коду. Если же у вас Unicode версия, то придется внести некоторые изменения в исходники TBassPlayer, в его главный модуль, иначе работать не будет (у меня в D2009 возникали ошибки во время выполнения, связанные с некорректным форматом строк). Хотя в последней его версии и указана совместимость с D2009, да и по времени выхода (май 2009) уже по идее должна быть поддержка юникода, но мне пришлось лезть в исходники даже последней версии. Исправленный модуль вы найдете в файлах к статье [1].
Добавлю еще, что помимо функций оболочки над <bass.dll>, TBassPlayer добавляет поддержку Winamp — плагинов, визуализации и другие функции. Подробнее читайте в справке к компоненту.
Итак, вернемся к нашим баранам. Скопируйте в папку с проектом файл <Spectrum.pas> с нашим анализатором и подключите его в Uses. Я не стал устанавливать в среду TBassPlayer, не люблю я все подряд устанавливать, а просто включил его в Uses. Пути к компоненту можете указать в свойствах среды, а в директорию с программой нужно скопировать файл <bass.dll> подходящей версии (разные версии TBassPlayer работают с разной версией BASS, последняя версия компонента 2.1 может работать с последним BASS 2.4.5). В обработчике события OnCreate() главной формы создаем экземпляр TBassPlayer и назначаем ему на событие OnNewFFTData() процедуру DisplayFFTBand. Затем создаем объект анализатора и назначаем ему количество каналов соответствующее количеству каналов в TBassPlayer (константа NumFFTBands).
Код открытия файла и запуска я скопировал из «демки» к TBassPlayer, заодно там и отображение некоторых свойств файла я оставил, будем выводить их в метки TLabel. Своего я добавил только кнопку Play/Pause, для определения режима работы кнопки используется ее поле Tag (меняем с «0» на «1» попеременно), при нажатии кнопки читаем ее Tag и производим необходимые действия: меняем название, запускаем или останавливаем воспроизведение и меняем Tag.
Кроме этого, мы создаем процедуру ClearChannels. В ней происходит обнуление каналов, это будет происходить при паузе и остановке. Ну и самое интересное для нас – это метод DisplayFFTBand, который привязан к событию обновления данных спектра, в него приходит состояние каналов в виде объекта Bands: TBandOut. Мы просто перегоняем значения в наш Analizer.Channels и запускаем обновление Analizer.Draw. Вот и все. Как видите, подключение элементарное, инкапсуляция проявляет себя во всей красе, все детали скрыты в модулях с классами, и нужно сделать всего несколько телодвижений, для того чтобы приложение заработало.
BASS
Пример кода подключения к <bass.dll> и получения данных спектра любезно предоставил наш форумчанин Зарипов Равиль (ZuBy). Кстати он разрабатывает плеер на базе BASS, можете подробнее ознакомиться на его сайте www.zubymplayer.com.
Библиотеку BASS и API для разных языков вы можете скачать с официального сайта проекта www.un4seen.com.
В поставке с BASS идет оболочка для Delphi <bass.pas>. Скопируйте ее в папку с проектом и подключите ее в Uses. Тоже самое проделайте с <Spectrum.pas>. По сути, подключение анализатора мало отличается от случая с TBassPlayer. Также создаем объект, назначаем ему родителя, координаты и количество каналов.
Небольшие отличия есть в механизме получения данных о спектре. Если кто не знает, FFT – означает Fast Fourier Transorm, по-русски – Быстрое Преобразование Фурье или БПФ. Таким образом, FFT Data переводится как БПФ данные. В таймере мы получаем от BASS эти самые БПФ данные в массив FData и запускаем процедуру SpectrumRender. В ней проверяется, остановлено ли воспроизведение, если остановлено, то обнуляется массив FData. В связи с этим хочу заметить, что в этом случае наш таймер в классе TAnalizer является «пятым колесом», так как отрисовка спектра будет вызвана все равно, даже если воспроизведение остановлено. Он не мешает (нагрузку большую он не дает), но и не помогает, если хотите, можете встроить в класс команду по его отключению или вообще убрать таймер, тогда немного переделать функцию Draw нужно будет. Однако, в случае с TBassPlayer собственный таймер нам пригодился.
Но вернемся к процедуре – SpectrumRender. После проверки на остановку, мы заполняем значениями каналы анализатора и вызываем его отрисовку. В отличие от случая с TBassPlayer, где мы просто переносили значения без изменений, здесь мы имеем сырые БПФ данные, и расшифровку их мы должны делать самостоятельно. В данном случае мы берем, из массива с БПФ, данные, начиная с 5-го элемента, в количестве равном количеству каналов в спектрограмме. Правильно это или нет, я не знаю, и похоже мало кто вообще знает как правильно**, и в основном все возможно больше ориентируются на красивый вид спектрограммы, чем на точные значения частот. В любом случае, мы каналы не подписываем точным значением частоты, так что с нас взятки гладки.
Автор TBassPlayer применяет другой подход. Можете ознакомиться с ним в исходниках к компоненту [22]. А так как цель статьи – показать, как можно вывести на экран спектрограмму, а не разбор БПФ данных, то на этом скажем Фурье до-свидания и продолжим рассмотрение процедуры.
Итак, мы определились, какие элементы из массива будем брать. Затем мы умножаем значение на коэффициент, найденный пробным путем, он зависит от высоты спектрограммы. Судя по тому, что к полученному значению применена функция модуля Abs, значения могут иногда быть и отрицательными, оставляем это без изменений (напоминаю, что эта часть кода не моя, я только прикрутил сюда заполнение анализатора). Откидываем дробную часть с помощью функции Trunc(), и можем заполнять этими значениями каналы нашей спектрограммы. После заполнения вызываем отрисовку методом Analizer.Draw.
Таким образом, при каждом срабатывании таймера, анализатор будет заполняться новыми значениями и будет вызываться его отрисовка. В случае, если трек будет остановлен или поставлен на паузу, так же будет заполняться анализатор, только в этом случае нулями, и так же будет вызываться отрисовка. Это позволит нам увидеть, как пики плавно падают вниз. На этом закончим рассмотрение подключения к BASS.
Заключение
Итак, мы создали свой класс спектрограммы, который умеет вывести себя на экран, как по событию перерисовки, так и по таймеру, к тому же он еще и не мерцает при отрисовке и имеет удобный интерфейс. Также мы имеем образец повторно используемого кода, мы ведь его уже использовали как минимум три раза в разных проектах, первый раз в тесте, второй – при подключении к TBassPlayer, и третий раз при подключении напрямую к BASS.
Исходники тестовых модулей получения спектрограмм прилагаются в виде ресурсов в теме «Журнал клуба программистов. Третий выпуск» или непосредственно в архиве с журналом.
Ресурсы
- Типичные примеры обсуждения на форуме по получению спектрограммы через библиотеку BASS
http://www.programmersforum.ru/showthread.php?t=71069&highlight=%FD%EA%E2%E0%EB%E0%E9%E7%E5%F0
http://www.programmersforum.ru/showthread.php?t=89708&highlight=%FD%EA%E2%E0%EB%E0%E9%E7%E5%F0
http://www.programmersforum.ru/showthread.php?t=94224&highlight=%FD%EA%E2%E0%EB%E0%E9%E7%E5%F0
Модули и проекты, использованные в статье http://programmersclub.ru/pro/pro3.zip. Модули и проекты, использованные в статье http://programmersclub.ru/pro/pro3.zip
Статья из третьего выпуска журнала «ПРОграммист».
Облако меток
css реестр ассемблер timer SaveToFile ShellExecute программы массив советы word MySQL SQL ListView pos random компоненты дата LoadFromFile form база данных сеть html php RichEdit indy строки Win Api tstringlist Image мысли макросы Edit ListBox office C/C++ memo графика StringGrid canvas поиск файл Pascal форма Файлы интернет Microsoft Office Excel excel winapi журнал ПРОграммист DelphiКупить рекламу на сайте за 1000 руб
пишите сюда - alarforum@yandex.ru
Да и по любым другим вопросам пишите на почту
пеллетные котлы
Пеллетный котел Emtas
Наши форумы по программированию:
- Форум Web программирование (веб)
- Delphi форумы
- Форумы C (Си)
- Форум .NET Frameworks (точка нет фреймворки)
- Форум Java (джава)
- Форум низкоуровневое программирование
- Форум VBA (вба)
- Форум OpenGL
- Форум DirectX
- Форум CAD проектирование
- Форум по операционным системам
- Форум Software (Софт)
- Форум Hardware (Компьютерное железо)