Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Язык программирования Сpp 25.09.11 (2).doc
Скачиваний:
16
Добавлен:
19.08.2019
Размер:
10.09 Mб
Скачать

Создание мультфильма

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

Координаты выводимого символа обозначим через x и y. Если организовать вывод на экран символа и при этом горизонтальная координата будет со временем уменьшаться, а вертикальная останется постоянной, то символ постепенно будет перемещаться влево. Точнее, новый сивол будет появляться левее, чем выведенный до него. Если теперь односременно с выводом нового символа стирать старый, то это вызовет эффект перемещения символа справа налево. Для содания такой программы поступим следующим образом. Разместим на форме кнопку и таймер из вкладки System. Таймер – это невизуальный компонент, который может размещаться в любом месте формы. Он имеет два свойства, позволяющие им управлять: Interval –интервал в милисекундах и Enabled — доступность. Свойство Interval задает период срабатывания таймера. Через заданный интервал времени после предыщего срабатывания, или после программной установки свойства или после запуска приложения, таймер срабатывает, вызывая событие OnTimer.

В обработчике события OnClicl кнопки разместим простой код

x=Form1->Width;

Timer1->Enabled=!Timer1->Enabled;

Идентификатор x – целесообразно объявить как глобальную перемену, т.е. объявление

int x;

сделать сразу после директив препроцессора. Там же разместить функцию

void __fastcall TForm1::out(int x){

Form1->Canvas->TextOut(x,100,"a");

Form1->Canvas->TextOut(x+8,100," ");

}

которая с помощью метода TextOut выводит на форму сивол «a» в точке с текущей координатой x и постоянной координатой y=100. Ширина символов, которые выводятся на поверхность формы, составляет около 8 пикселей. Поэтому вывод двух пробелов смещенных на 8 пикселей вправо, позволяет гарантировано стиреть предыдущий символ.

Для того чтобы функция out работала, ее прототип нужно объявить в заголовочном файле, который расположен в том же окне на вкладке Unit1.h. В проивном случае для главной функции WinMain()функция out не будет объявленной. Прототип нужно расположить в классе TForm1.

Вернемся к обработчику кнопки. Щелчек по кнопке меняет состояние таиймера и в то же время порождает событие обращение к таймеру OnTimer. Обработчтк таймера устроен так, что после задержки указанной в его свойстве Interval он выполняет записанный в обработчике код, затем цикл повторяется, т.е. вновь происходит остановка в выполнении программы, после чего исполняется код обработчика, и.т., до тех пор, пока не произойдет новое событие. Если теперь в обработчике таймера вызывать функцию out(x) и изменять значение координаты x, говоря иначе, заприсать код

out(x);

x=x-8;

то выполнение кода будет исполняться с задержкой Interval. Устанавливае Interval в пределах 50-200 милисекунд можно добиться необходимой скорости движения символа.

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

Понятно, что эта задача похожа на предыдущую, только после передвижения строки должно происходить ее стирание. Сделать это несложно. Добавьте в объявление глобальную переменную – строку, например,

String st="Hello!";

Внесите исправления. созданную ранее функии out

void __fastcall TForm1::out(int x){

Form1->Canvas->TextOut(x+27,100," ");

Form1->Canvas->TextOut(x,100,st);

}

Тепрерь имея некоторый опыт попробуем создать настоящий мультфильм. Конечно, рисунки такого фильма будут довольно простыми. Они будут выполнены с помощью свойства Pen, описанного ранее.

Откроем новое приложение и поместим на него компоненты Image, Timer и Button. Последние разместим внизу формы. Таймер будет задавать темп смены кадров. Значение параметра Interval компонента Timer зададим равным 500 миллисекундам. Значение параметра Enabled следует сделать равным false.

В нижней части окна формы щелкните по закладке заголовочного файла. Если файлу еще не дано имя, то по умолчанию это Unit1.h.

Закладка заголовочного файла

В текст заголовочного файла Unit1.h следует добавить строку

void __fastcall Draw();

Это объявление функции, которая будет рисовать изображение. А текст самого модуля Unit1.cpp должен начинаться с объявлений:

short int num =0;

short int H=20; // шаг

short int Xpos=2*H; // координата туловища

short int Ypos=120; // "земля"

short int Hmen=30; // высота тела

short int Rhead=10; // радиус головы

short int Rhead2=Rhead/2; // радиус литавров

short int revers=1; // направление движения

short int L=H*1.41; // длина ноги

//--------------------------------------------------------------------

В код основного файла Unit1.cpp внесите следующий код:

void __fastcall TForm1::Draw (){

short int Yhead; // координата низа головы

switch (num){

case 0:

Yhead=Ypos-H-Hmen ;

Image1->Canvas->MoveTo(Xpos-H,Ypos);

Image1->Canvas->LineTo(Xpos, Ypos-H); // нога

Image1->Canvas->LineTo(Xpos+H, Ypos); // другая нога

Image1->Canvas->MoveTo(Xpos, Ypos-H) ;

Image1->Canvas->LineTo(Xpos, Yhead); // туловище

Image1->Canvas->MoveTo(Xpos+revers*H, Yhead-H) ;

Image1->Canvas->LineTo(Xpos,Yhead+4); // рука

Image1->Canvas->Ellipse(Xpos+revers*H-Rhead2,Yhead-H-Rhead2,

Xpos+revers*H+Rhead2, Yhead-H+Rhead2);

Image1->Canvas->LineTo(Xpos+revers*H, Yhead+H); // другая рука

Image1->Canvas->Ellipse(Xpos+revers*H-Rhead2, Yhead+H-Rhead2,

Xpos+revers*H+Rhead2, Yhead+H+Rhead2);

Image1->Canvas->Ellipse(Xpos-Rhead, Yhead,Xpos+Rhead,

Yhead-2*Rhead);

Image1->Canvas->Rectangle(Xpos-Rhead, Yhead-2*Rhead-1,

Xpos+Rhead, Yhead-2*Rhead-4); // шляпа

Image1->Canvas->Rectangle(Xpos-Rhead+2, Yhead-2*Rhead-4,

Xpos+Rhead-2, Yhead-2*Rhead-10); // верх шляпы

break;

case 1:

Yhead=Ypos-L-Hmen;

Image1->Canvas->MoveTo(Xpos, Ypos);

Image1->Canvas->LineTo(Xpos, Yhead);

Image1->Canvas->MoveTo(Xpos, Yhead+4);

Image1->Canvas->LineTo (Xpos+revers*L, Yhead+4);

Image1->Canvas->Ellipse (Xpos+revers*L-Rhead2, Yhead+4-Rhead2,

Xpos+revers*L+Rhead2, Yhead+4+Rhead2);

Image1->Canvas->Ellipse (Xpos-Rhead, Yhead, Xpos+Rhead,

Yhead-2*Rhead);

Image1->Canvas->Rectangle(Xpos-H/2, Yhead-2*Rhead-1,

Xpos+H / 2,Yhead-2*Rhead-4);

Image1->Canvas->Rectangle(Xpos-H/2+2, Yhead-2*Rhead-4,

Xpos+H/2-2,Yhead-2*Rhead-10);

}

}

//------------------------------------------------------

Обработчик события OnTimer выглядит так:

void __fastcall TForml::Timer1Timer(TObject *Sender){

Draw();

if ((Xpos>=Image1->Picture->Width-H) || (Xpos<=H))

revers=-revers;

Xpos=Xpos+revers*H;

num=1-num;

Draw();

}

//---------------------------------------------------------

Обработчик события кнопки OnClick содержит всего лишь один оператор, который меняет сотояние таймера на обратное

void __fastcall TForm1::Button1Click(TObject *Sender)

{

Timer1->Enabled=!Timer1->Enabled;

}//--------------------------------------------------------

В обработчик события формы включите FormCreate следующий код:

void __fastcal1 TForm1::FormCreate(TObject *Sender){

Image1->Canvas->MoveTo(0,Ypos+3) ;

Image1->Canvas->Pen->Width = 4;

Image1->Canvas->LineTo(Image1->ClientWidth,Ypos+3); // земля

Image1->Canvas->Pen->Width=1;

Image1->Canvas->Pen->Mode=pmNotXor; /* инверсный pmMerge

pmMerge - логическое or между цветом пера и фоном - возвращает 1, если оба бита не равны 0. Приводит к осветлению.

*/

Draw();

}

Начнем анализ этого кода с конца — с последней процедуры FormCreate, которая является обработчиком события OnCreate формы. В этой процедуре рисуется линия, отображающая «землю», по которой будет ходить наш человечек. Затем устанавливается режим пера pmNotXor, который говоит о том, что если пиксел принадлежит рисунку и фону, то все биты цвета пиксела меняются на противоположные. И в заключение, вызывается функция Draw, которая рисует исходное положение человечка.

Функция OnClick является обработчиком события кнопки. Каждый щелчок на кнопке включает или выключает таймер, в результате чего человек идет или останавливается. Функция Timer1Timer является обработчиком события OnTimer таймера. Это событие означает, что надо стереть прежний кадр и нарисовать новый. Сначала вывается функция Draw. Поскольку позиция человечка с момента показа предыдущего кадра не изменилась, то этот вызов рисует на том же самом месте, котором рисовался предыдущий кадр. Следовательно, предыдущий рисунок стирается. Затем анализируется позиция человечка Xpos. Если эта позиция отстоит от какого-либо конца холста Imagel на величину, меньшую шага Н, то изменяет на обратный знак переменной revers, характеризующей направление движения. Если revers=1, человечек шагает вправо; если revers=-1, человечек шагает влево. Затем позиция Xpos изменяется на величину revers, т.е. на шаг вправо или влево. Изменяется переменная num, которая указывает номер высвечиваемого кадра: 0 или 1. В заключение вызывается процедура Draw, которая рисует указанный кадр в указанной позиции.

Последняя функция, которую мы рассмотрим - функция Draw, рисующая кадр. Она достаточно длинная, но в ней нет ничего сложного. В зависимости от значеиия num рисуется один или другой кадр, причем в рисунке учитывается позициция Xpos и направление движения revers.

Сохраните ваше приложение и выполните его. Щелкнув на кнопке, вы можете заставить вашего человечка перемещаться. Достигнув края формы, он будет поворачиваться, и шагать в обратном направлении. При вторичном щелчке на кнопн он будет останавливаться.

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

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

Image1->Canvas->Brush->Color=0;

Image1->Canvas->Rectangle(90,0,200,100);

Image1->Canvas->Brush->Color=clWhite;

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

В рассмотренном примере это можно сделать так: Откройте свой предыдущий проект мультипликации и сохраните его под новым именем командой File|Save Project As. Затем сохраните под новым именем файл модуля командой File|Save As. Теперь вы можете вводить в модуль изменения, не опасаясь испортить свой предыдущий проект.

Введите глобальную переменную типа TBitmap:

Graphics::TBitmap *BitMap;

Это будет объект, в котором вы будете сохранять фрагменты фона, испорченные очередным кадром. Переделайте процедуру FormCreate следующим образом:

void fastcall Tform1::FormCreate(TObject *Sender)

{

BitMap=new Graphics::TBitmap;

BitMap->Width=2*(L + Rhead);

BitMap->Height=L+Hmen+2*Rhead+6;

Image1->Canvas->MoveTo (0, Ypos+3);

Image1->Canvas->Pen->Width=4;

Image1->Canvas->LineTo(Image1->ClientWidth, Ypos+3); // земля

Image1->Canvas->Pen->Width=2;

BitMap->Canvas->CopyRect(Rect(0,0,BitMap->Width,BitMap->Height),

Imagel->Canvas,Rect(Xpos-L-Rhead,Ypos-(L+Hmen+2*Rhead+5), Xpos+L+Rhead,Ypos+1));

Draw(); }

Первыми операторами этой процедуры вы создаете объект BitMap и задаете его размеры равными максимальным размерам изображения человечка. В конце процедуры, перед вызовом Draw в компонент BitMap методом CopyRect копируется фрагмент изображения, внутри которого будет расположен рисунок человечка.

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

Поскольку приложение создало объект Bitmap, надо не забыть добавить в него обработчик события OnDestroy формы, в который вставить оператор

Bitmap->Free();

Процедуру Timer1Timer измените следующим образом:

void __fastcall Tform1::Timer1Timer(TObject *Sender){

Image1->Canvas->Draw(Xpos-L-Rhead,

Ypos-L-Hmen-2*Rhead-5, BitMap);

if ((Xpos >=Image1->Picture->Width-H) | (Xpos <= H))

revers=-revers;

Xpos=Xpos+revers*H;

num=1-num;

BitMap->Canvas->CopyRect(Rect(0, 0 , BitMap->Width,

BitMap->Height),Imagel->Canvas,

Rect(Xpos-L-Rhead,

Ypos-(L+Hmen+2*Rhead+5),

Xpos+L+Rhead,Ypos+1));

Draw () ;

}

Если вы сравните с тем, что было в предыдущем приложении, то увидите, чтовместо первого вызова процедуры Draw, который стирал предыдущий кадр, вводится оператор Image1->Canvas->Draw, который выполняет ту же функцию, но путем восстановления запомненного ранее фрагмента фона под рисунком. Вторым отличием является наличие оператора BitMap—>Canvas—>CopyRect, который перед вызовом Draw запоминает новый фрагмент фона.

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

Наше изображение было очень простым и рисовалось быстро. Но при сложных Изображениях время рисования может быть заметным и приводить к мерцанию картинки и другим неприятным зрительным эффектам. В этих случаях используем буферизацию изображения. Это напоминает то, что вы только что делали с фоном, но относится не к фону, а к рисунку. Рисование очередного кадра производится не сразу на холсте, который видит пользователь, а на канве невидимого компонента, типа того Bitmap, с которым вы только что работали. А после того, как рисунок сделан, он переносится на видимый холст методами Draw или CopyRect. Эти методы работают очень быстро, и никаких неприятных мерцаний не происходит.

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

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

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

#define Pi 3.1415926535897932385

short int cadr =0; // номер кадра

short int H=30; // длина ноги и руки

short int Xpos = 3 * H;, // координата опорной ноги

short int Ypos =120; // "земля"

short int Hmen =3 0; // высота тела

short int Rhead =10; // радиус головы

short int revers =1; // направление движения

short int L = H * 1.41; // длина ноги

short int Ncadr=16; // число кадров на шаг

//-------------------------------------------------

void fastcall Tform1::Draw(){

float Angl=Pi/4*(1+(2.*cadr)/(Ncadr-1));

short int Yb=Ypos-H*sin(Angl);

short int Yt=Yb-Hmen;

short int X=Xpos-revers*H*cos(Angl);

Image1->Canvas->MoveTo(X-(Xpos-X),Ypos);

Image1->Canvas->LineTo(X,Yb); // нога

if(cadr !=Ncadr/2-1)

Image1->Canvas->LineTo(Xpos,Ypos); // другая нога

Image1->Canvas->MoveTo(X,Yb);

Image1->Canvas->LineTo(X,Yt); .. // туловище

short int XI = X - revers * O(b-Ypos);

Image1->Canvas->MoveTo(XI,Yt+5-(Xpos-X));

Image1->Canvas->Ellipse(Xl-Rhead/2,

Yt+5-(Xpos-X)-Rhead/2, Xl+Rhead/2, Yt+5-(Xpos-X)+Rhead/2);

Image1->Canyas->LineTo(X,Yt+5); // рука

if (cadr !=Ncadr/2-1) {Imagel->Canvas->Ellipse(Xl-Rhead/2,

Yt+5+(Xpos-X)-Rhead/2, Xl+Rhead/2,

Yt+5+(Xpos-X)+Rhead/2);"

Image1->Canvas->LineTo(XI,Yt+5+(Xpos-X)); // другая рука }

Image1->Canvas->Ellipse(X-Rhead,Yt-2*Rhead, X+Rhead, Yt);

Image1->Canvas->Rectangle(X-Rhead,Yt-2*Rhead-4,

X+Rhead,Yt-2*Rhead-l); //шляпа

}

void __fastcall TForml::BRunClick(TObject *Sender){

Timer1->Enabled=!Timerl->Enabled;

}

//------------------------------------------------

void __fastcall Tform1::Timer1Timer(TObject *Sender){

Draw();

cadr=(cadr+l)%Ncadr;

if((cadr==0)

if((Xpos<=Image1->(Xpos->Picture->Width-revers*3*H)&&(Xpos + =revers*H* 1.41;

else revers = -revers;

Draw() ;

}

//------------------------------------------------I

void __fastcall Tform1::FormCreate(TObject *Sender){

Image1->Canvas->MoveTo(0,Ypos+3);

Image1->Canvas->Pen->Width = 4;

Image1->Canvas->LineTo(Image1->ClientWidth, Ypos + 3); // земля

Image1->Canvas->Pen->Width = 1;

Image1>Canvas->Pen->Mode = pmNotXor;

Image1->Interval = 600/Ncadr;

В этом коде использованы математические функции sin и cos. Поэтому в код необходимо добавить директиву препроцессора

#include <math.h>

Рассмотрим коротко приведенный код.

Процедура FormCreate отличается от той, что была в первом приложении только оператором, задающим выдержку таймера (свойство Interval) путем деления 600 на константу Ncadr, которая задает число кадров на один цикл движе на один шаг человечка. Как видно, длительность одного шага выбрана рав 600 миллисекунд. Вы, конечно, можете изменить это значение, как и значение Ncadr, выбранное равным 16.

Процедура BRunClick не отличается от той, что была в первом приложении Процедура Timer1Timer, как и в первом приложении начинается и кончается вызовами Draw, первый из которых стирает изображение предыдущего кадра, а второй рисует новый кадр. После первого вызова Draw рассчитывается значени переменной cadr оператором

сadr=(cadr+l)%Ncadr;

Поскольку тут используется операция вычисления остатка от деления cadr+1 на Ncadr, то значение cadr последовательно получает значения 0, 1, 2, ..., Ncadr-1. Шаг начинается с cadr = 0. При этом проверяется, не приблизился ли человечек к краю формы, и если приблизился — изменяется направление движения (знак переменной revers).

Наиболее серьезно изменилась процедура Draw. Она начинается с определения угла наклона ног и рук Angl, исходя из номера кадра:

float Angl=Pi/4"(1+(2.*cadr)/(Ncadr-1)) ;

Обратите внимание на то, что константа 2 в этом выражении записана с точкой и произведение 2.*cadr заключено в скобки. Это принципиально, так как в этом случае константа воспринимается как значение с плавающей запятой и все мыражение (2.*cadr)/(Ncadr-1) вычисляется как значение с плавающей точкой. Если не поставить точку после 2, то будут применяться целочисленные вычисления и пока 2 * cadr меньше, чем Ncadr-1, значения этого выражения буду равны 0 и угол изменяться не будет.

После вычисления угла на его основе строится изображение, подобное тому которое было в первом приложении. Обратите внимание на то, что вторая нога и вторая рука человечка рисуются только, если выполняется условие cadr !=Ncadr /2-1. Это связано с тем, что, если число кадров Ncadr четное, то в этот момент одна нога накладывается на другую и руки также накладываются друг надруга. Поскольку рисование идет в режиме pmNotXor, то это наложение приведет к тому, что у человечка вообще исчезнут в этом кадре руки и ноги. Правда всего на один кадр, но все равно неприятно.

Ваше приложение готово. Можете сохранить его и выполнить. Вы у увидите, что движения человечка стали плавными.