Полная версия
Справочник Жаркова по проектированию и программированию искусственного интеллекта. Том 7: Программирование на Visual C# искусственного интеллекта. Издание 2
Рис. 4.1. Форма. Рис. 4.2. Файл рисунка в SE (слева) и в Properties (справа).
Добавляем в проект (из отмеченной выше статьи или из Интернета) файл изображения сыра cheese.jpg по стандартной схеме, а именно: в меню Project выбираем Add Existing Item, в этой панели в окне “Files of type” выбираем “All Files”, в центральном окне находим и выделяем имя файла и щёлкаем кнопку Add (или дважды щёлкаем по имени файла).
В панели Solution Explorer мы увидим этот файл (рис. 4.2).
Теперь этот же файл cheese.jpg встраиваем в проект в виде ресурса по разработанной выше схеме, а именно: в панели Solution Explorer выделяем появившееся там имя файла, а в панели Properties (для данного файла) в свойстве Build Action (Действие при построении) вместо заданного по умолчанию значения Content (Содержание) или None выбираем значение Embedded Resource (Встроенный ресурс).
Для написания программы, в самом верху файла Form1.cs записываем пространство имён System.Reflection для управления классом Assembly:
using System.Reflection; //For the Assembly class.
В панели Properties (для Form1) на вкладке Events дважды щёлкаем по имени события Paint. Появившийся шаблон метода Form1_Paint после записи нашего кода принимает следующий вид.
Другие варианты вывода изображения, например, на элемент управления PictureBox и после щелчка по какому-либо элементу управления уже приводились ранее.
Листинг 4.1. Метод для построения изображения.
//We declare the object of class System.Drawing.Image
//for a product:
Image cheeseImage;
//We load into the project the image files according
//to such scheme:
//We create an object myAssembly of the Assembly class
//and appropriate to it
//the link to the executed assembly of our application:
static Assembly myAssembly = Assembly.GetExecutingAssembly();
//We create the myAssemblyName object of
//the System.Reflection.AssemblyName class and appropriate to
//it the assembly name, which consists of a project name,
//Version, Culture, PublicKeyToken:
static AssemblyName myAssemblyName = myAssembly.GetName();
//From the assembly name by means of the Name property
//we select a string project name:
static string myName_of_project = myAssemblyName.Name;
private void Form1_Paint(object sender, PaintEventArgs e)
{
//We load into object of the System.Drawing.Image class
//the image file of the set format, added to the project,
//by means of ResourceStream:
cheeseImage =
new Bitmap(myAssembly.GetManifestResourceStream(
myName_of_project + "." + "cheese.JPG"));
//We draw the image on the Form1:
e.Graphics.DrawImage(cheeseImage, 10, 20);
}
Строим и запускаем программу на выполнение обычным образом:
Build, Build Selection; Debug, Start Without Debugging.
Появляется форма Form1 с изображением типа встроенного нами рисунка сыра cheese.jpg (рис. 4.1).
Верхний левый угол изображения по отношению к верхнему левому углу экрана (где находится начало координат) расположен в соответствии с заданными нами координатами в строке кода (e.Graphics.DrawImage(myBitmap, 10, 20);).
4.2. Методика анимации объекта
Программа может рисовать теперь сыр на экране. Затем она должна перемещать сыр, неоднократно рисуя и перерисовывая изображение сыра в различных позициях. Если программа делает это достаточно быстро, создаётся иллюзия движения (анимация).
Следующий пример кода создаёт метод updatePositions, который перемещает сыр. На данной стадии проектирования сыр будет только двигаться вправо и вниз (по осям координат “x” и “y”). Таким образом, добавляем в данный (или новый) проект такой код.
Листинг 4.2. Изменение координат продукта.
//Current abscissa of an object:
int cx = 50;
//Current ordinate of an object:
int cy = 100;
private void updatePositions()
{
cx++; //or cx = cx + 1;
cy++; //or cy = cy + 1;
}
Видно, что программа использует переменные cx и cy, чтобы задавать местоположение сыра. Сейчас их значения становятся больше на единицу каждый раз, когда вызывается обновление экрана, что заставляет сыр двигаться направо и вниз.
В процессе игры, для вызова метода updatePositions через одинаковые промежутки времени, целесообразно использовать таймер. С панели инструментов Toolbox размещаем на форме компонент Timer (Таймер). В панели Properties (для данного компонента Timer) в свойстве Enabled оставляем булево значение False, а свойству Interval задаём значение 40 (миллисекунд, что соответствует 25 кадрам в секунду по стандарту телевещания России; 1000 миллисекунд равно 1 секунде).
Важно отметить, что добавление в проект компонента Timer (Таймер) означает, что наша игра должна отключить таймер, когда игра находится в фоновом режиме, и включить таймер при активации игры. Именно поэтому в панели Properties (для данного компонента Timer) в свойстве Enabled мы оставили булево значение False.
Кроме того, таймер не должен быть включенным, пока программа не загрузит изображение. Поэтому в приведённый выше метод Form1_Paint дописываем в самом низу:
//We turn on the timer:
timer1.Enabled = true;
Окончательно, код в теле метода Form1_Paint должен иметь такой вид.
Листинг 4.3. Метод для рисования изображения.
private void Form1_Paint(object sender, PaintEventArgs e)
{
//We load into object of the System.Drawing.Image class
//the image file of the set format, added to the project,
//by means of ResourceStream:
cheeseImage =
new Bitmap(myAssembly.GetManifestResourceStream(
myName_of_project + "." + "cheese.JPG"));
//We draw the image on the Form1:
e.Graphics.DrawImage(cheeseImage, cx, cy);
//We turn on the timer:
timer1.Enabled = true;
}
Теперь всякий раз, когда вызывается метод Form1_Paint, программа рисует сыр на экране с соответствующими координатами cx и cy.
Дважды щёлкаем по значку для компонента Timer (ниже формы в режиме проектирования). Появляется шаблон метода timer1_Tick, который после записи нашего метода updatePositions и библиотечного метода Invalidate (или Refresh) для перерисовки изображения на экране принимает следующий вид.
Листинг 4.4. Метод для смены кадров на экране и перемещения фигуры.
private void timer1_Tick(object sender, EventArgs e)
{
//We call the method:
updatePositions();
//We redraw the screen:
Invalidate();
}
Строим и запускаем программу на выполнение обычным образом:
Build, Build Selection; Debug, Start Without Debugging.
В ответ Visual C# выводит форму Form1 в режиме выполнения, на которой изображение типа встроенного нами рисунка сыра cheese.jpg перемещается из верхнего левого угла по диагонали сверху вниз (в нижний правый угол) и скрывается (рис. 4.3).
Рис. 4.3. Объект перемещается по диагонали сверху вниз. Рис. 4.4. Отскок объекта.
Изображение объекта мерцает, что в дальнейшем будет исправлено применением двойной буферизации.
Таким образом, мы разработали методику анимации, по которой можно перемещать любые объекты на экране .
4.3. Методика проектирования отскока объекта от границы
Разработаем методику решения задачи по отскоку заданного нами объекта от заданных нами границ, например, от границ экрана. В качестве предмета и замкнутого пространства могут быть, например:
резиновый мяч, металлический или пластмассовый шар, который с большой силой бросил человек в каком-то помещении; предмет летает внутри помещения и отскакивает от пола, потолка и стен этого помещения;
пуля, выпущенная из огнестрельного оружия, например, стальная дробь, выпущенная из охотничьего ружья в комнате и в полёте отскакивающая от пола, потолка и стен этой комнаты.
На практике подобные очень сложные задачи решаются после ввода в постановку задачи большого числа допущений.
Мы также введём большое число допущений, после чего задачу формулируем таким образом:
решаем плоскую задачу, т.е. предмет изображаем в виде его проекции на плоскость “x, y”; в качестве примера предмета выбираем кусочек сыра cheese.jpg, проекция которого на плоскость имеет вид прямоугольника;
на этой плоскости “x, y” замкнутое пространство изображаем в виде задаваемой нами замкнутой линии; в качестве примера замкнутой линии выбираем прямоугольник границы экрана;
предмет перемещается в этой плоскости “x, y” до столкновения с границей (линией), а после удара о границу должен отскочить от границы под определённым углом и перемещаться до следующего столкновения с границей, и так далее перемещаться и отражаться от линии;
принимаем обычное допущение, что до столкновения с границей предмет перемещается (летит) по прямой линии;
на основании допущения о том, что угол падения равен углу отражения, принимаем, что после столкновения с линией прямоугольника предмет отскакивает от этой линии под тем же углом; величину угла падения и угла отражения предмета от прямой линии принимаем равной 45 градусам;
перемещение предмета осуществляется поэтапно за интервал времени, который мы установим с помощью компонента Timer (Таймер);
интервал времени устанавливаем по значению свойства Interval компонента Timer; таким образом скорость перемещения объекта можно изменять за счёт изменения свойства Interval;
анимация является бесконечным (если в него не вмешиваться) нециклическим процессом; анимацию можно остановить на любом этапе и запустить вновь.
Для решения этой задачи программа должна отслеживать текущую позицию (в виде координат) объекта, и затем, когда значение одной из двух координат объекта станет равным значению одной из двух координат границы, изменить координаты объекта в противоположном от границы направлении.
Таким образом, в данном проекте приведённый выше метод updatePositions заменяем на следующий.
Листинг 4.5. Отскок объекта от границ.
//Movement on an axis "x" to the right:
bool goingRight = true;
//Movement on an axis of "y" to the down:
bool goingDown = true;
private void updatePositions()
{
if (goingRight)
{
cx++;
}
else
{
cx–;
}
if ((cx + cheeseImage.Width) >= this.ClientSize.Width)
{
goingRight = false;
}
if (cx <= 0)
{
goingRight = true;
}
if (goingDown)
{
cy++;
}
else
{
cy–;
}
if ((cy + cheeseImage.Height) >= this.ClientSize.Height)
{
goingDown = false;
}
if (cy <= 0)
{
goingDown = true;
}
}
В этом коде видно, что координаты объекта “x, y” изменяются на величину +1, когда объект перемещается в положительном направлении осей “x, y” (вправо и вниз), и изменяются на величину -1, когда объект перемещается в отрицательном направлении осей “x, y” (влево и вверх).
Код использует свойства ширины и высоты объекта (cheeseImage.Width и cheeseImage.Height) и экрана this.ClientSize.Width и this.ClientSize.Height). Вследствие этого программа будет нормально работать для любых размеров объекта и экрана.
В режиме выполнения (Build, Build Selection; Debug, Start Without Debugging) мы видим, что на форме Form1 изображение типа встроенного нами рисунка сыра cheese.jpg перемещается по диагоналям в различных направлениях, отскакивая от границ экрана (рис. 4.4).
Методика приостановки и возобновления анимации уже была приведена выше.
4.4. Методика управления скоростью перемещения объекта и добавления звукового сигнала
Предыдущая программа довольно медленно перемещает объект по экрану. Если ширина экрана, например, 100 пикселей, то с частотой 25 кадров в секунду объект пересекает этот экран по горизонтальной прямой приблизительно за 4 секунды. Для управления скоростью перемещения объекта вместо предыдущего кода, в котором изображение перемещается на 1 пиксель через каждый Interval времени срабатывания таймера, можно изменить количество пикселей xSpeed, на которое объект перемещается через каждый Interval времени срабатывания таймера, как показано в следующем коде:
if (goingRight)
{
cx += xSpeed;
}
else
{
cx -= xSpeed;
}
Изменяя значение xSpeed, можно увеличить или уменьшить горизонтальную составляющую (по оси “x”) скорости объекта.
Следующий аналогичный код для координаты “y” позволяет изменять вертикальную составляющую скорости объекта:
if (goingDown)
{
cy += ySpeed;
}
else
{
cy -= ySpeed;
}
Увеличивать или уменьшать скорость перемещения объекта можно при помощи переменной change в следующем методе:
private void changeSpeed(int change)
{
xSpeed += change;
ySpeed += change;
}
В этом коде целочисленная переменная change задана в виде параметра метода changeSpeed. Положительное значение переменной change увеличивает перемещение изображения через каждый Interval времени срабатывания таймера и, тем самым, увеличивает скорость, отрицательное – уменьшает.
Если мы хотим подавать звуковой сигнал в различные моменты анимации, например, в момент каждого удара объекта о границу (внутри которой перемещается объект), то поступаем следующим образом. Согласно разработанной выше методике использования в нашем приложении метода (функции) из любого другого языка, на первом этапе необходимо создать ссылку на тот язык, например, на Visual Basic. Для этого в меню Project выбираем команду Add Reference, в панели Add Reference на вкладке (.NET) выбираем ссылку Microsoft.VisualBasic и щёлкаем кнопку OK. А в соответствующий метод, например, updatePositions записываем строку:
Microsoft.VisualBasic.Interaction.Beep();
в тех местах, где нам нужен этот сигнал. Таким образом, в данном проекте приведённый выше метод updatePositions заменяем на следующий.
Листинг 4.6. Отскок объекта от границ.
//The current increment of movement on an axis "x":
int xSpeed = 1;
//The current increment of movement on an axis "y":
int ySpeed = 1;
//The method for increase in traverse speed:
private void changeSpeed(int change)
{
xSpeed += change;
ySpeed += change;
}
//The method for change of coordinates of an object:
private void updatePositions()
{
if (goingRight)
{
cx += xSpeed;
}
else
{
cx -= xSpeed;
}
if ((cx + cheeseImage.Width) >= this.ClientSize.Width)
{
goingRight = false;
//At time of collision, the sound signal Beep is given:
Microsoft.VisualBasic.Interaction.Beep();
}
if (cx <= 0)
{
goingRight = true;
//At time of collision, the sound signal Beep is given:
Microsoft.VisualBasic.Interaction.Beep();
}
if (goingDown)
{
cy += ySpeed;
}
else
{
cy -= ySpeed;
}
if ((cy + cheeseImage.Height) >= this.ClientSize.Height)
{
goingDown = false;
//At time of collision, the sound signal Beep is given:
Microsoft.VisualBasic.Interaction.Beep();
}
if (cy <= 0)
{
goingDown = true;
//At time of collision, the sound signal Beep is given:
Microsoft.VisualBasic.Interaction.Beep();
}
}
Для управления скоростью перемещения объекта воспользуемся каким-либо элементом управления или компонентом, например, наиболее распространённым элементом Button (Кнопка). С панели инструментов Toolbox размещаем на форме две кнопки Button и в панели Propertiesс в свойстве Text для левой кнопки записываем “Быстрее”, а для правой кнопки – “Медленнее”. Отметим, что для этих целей вместо кнопок Button (чтобы не загромождать форму) можно использовать и клавиши клавиатуры по описанной далее методике.
В режиме редактирования дважды щёлкаем по левой кнопке “Быстрее”.
Появившийся шаблон метода после записи одной строки (changeSpeed(1);) принимает следующий вид.
Листинг 4.7. Метод для изменения скорости объекта.
private void button1_Click(object sender, EventArgs e)
{
changeSpeed(1);
}
Аналогично дважды щёлкаем по правой кнопке “Медленнее”. Появившийся шаблон метода после записи одной строки (changeSpeed(-1);) принимает следующий вид.
Листинг 4.8. Метод для изменения скорости объекта.
private void button2_Click(object sender, EventArgs e)
{
changeSpeed(-1);
}
В режиме выполнения (Build, Build Selection; Debug, Start Without Debugging) мы видим, что на форме Form1 изображение типа встроенного нами рисунка сыра cheese.jpg перемещается в различных направлениях (рис. 4.5 и 4.6), отскакивая от границ экрана, а после выбора кнопок “Быстрее” или “Медленнее” этот объект перемещается соответственно быстрее или медленнее.
Причём, при каждом соприкосновении объекта с границей экрана мы слышим звуковой сигнал Beep.
Рис. 4.5. Перемещение объекта. Рис. 4.6. Перемещение объекта.
4.5. Методика добавления нового объекта в игру
Теперь, когда программа может отображать кусочек сыра cheese.jpg в динамике, добавляем второй объект игры, который, как ракетка в теннисе отбивает мяч, будет отбивать этот кусочек сыра. В качестве такого большего по размерам объекта выбираем батон белого хлеба (с которым обычно едят сыр).
Добавляем в проект (из отмеченной выше статьи или из Интернета) файл изображения батона хлеба bread.jpg по стандартной схеме, а именно: в меню Project выбираем Add Existing Item, в этой панели в окне “Files of type” выбираем “All Files”, в центральном окне находим и выделяем имя файла и щёлкаем кнопку Add (или дважды щёлкаем по имени файла). В панели Solution Explorer мы увидим этот файл.
Теперь этот же файл bread.jpg встраиваем в проект в виде ресурса по разработанной выше схеме, а именно: в панели Solution Explorer выделяем появившееся там имя файла, а в панели Properties (для данного файла) в свойстве Build Action (Действие при построении) вместо заданного по умолчанию значения Content (Содержание) или None выбираем значение Embedded Resource (Встроенный ресурс).
Объявляем и инициализируем объект breadImage (класса Image) для загрузки в него изображения хлеба и две текущие координаты bx и by верхнего левого угла прямоугольника, описанного вокруг хлеба, в системе координат с началом в верхнем левом углу экрана. А приведённый выше код в теле метода Form1_Paint заменяем на тот, который дан на следующем листинге.
Листинг 4.9. Метод для рисования изображения.
//We declare the object of class System.Drawing.Image
//for the subject:
Image breadImage; // = null by default.
//Current abscissa of a subject:
int bx = 0;
//Current ordinate of a subject:
int by = 0;
private void Form1_Paint(object sender, PaintEventArgs e)
{
//We load into the object of class System.Drawing.Image
//the image file of the set format, added to the project,
//by means of the ResourceStream:
cheeseImage =
new Bitmap(myAssembly.GetManifestResourceStream(
myName_of_project + "." + "cheese.JPG"));
breadImage =
new Bitmap(myAssembly.GetManifestResourceStream(
myName_of_project + "." + "bread.JPG"));
//We draw the images on the Form1:
e.Graphics.DrawImage(cheeseImage, cx, cy);
e.Graphics.DrawImage(breadImage, bx, by);
//We turn on the timer:
timer1.Enabled = true;
}
В режиме выполнения (Build, Build Selection; Debug, Start Without Debugging) мы видим, что на форме Form1 к перемещающемуся изображению сыра cheese.jpg добавилось изображение хлеба bread.jpg (в верхнем левом углу экрана), рис. 4.7.
Рис. 4.7. Подвижный сыр и неподвижный хлеб. Рис. 4.8. Сыр закрывает хлеб.
Однако изображения и сыра, и хлеба мерцают, что необходимо исправить методом двойной буферизации (в следующем параграфе).
4.6. Методика устранения мерцания изображения при помощи двойной буферизации
Идея устранения мерцания изображения методом двойной буферизации заключается в том, что сначала изображение проектируют не на экране, как до применения двойной буферизации, а в специальном буфере в памяти компьютера, а когда изображение полностью спроектировано в буфере памяти, оно копируется на экран . Так как процесс копирования готового изображения из буфера на экран происходит быстрее, чем процесс прорисовки изображения сразу на экране без использования промежуточного буфера, то мерцание изображения исчезает.
Чтобы устранить мерцание изображения при помощи двойной буферизации, приведённый выше код в теле метода Form1_Paint заменяем на тот, который дан на следующем листинге (с подробными комментариями).
Листинг 4.10. Метод для рисования изображения.
//Buffer in the view of object of class Bitmap:
Bitmap backBuffer = null;
private void Form1_Paint(object sender, PaintEventArgs e)
{
//We load into object of class System.Drawing.Image
//the image file of the set format, added to the project,
//by means of ResourceStream:
cheeseImage =
new Bitmap(myAssembly.GetManifestResourceStream(
myName_of_project + "." + "cheese.JPG"));
breadImage =
new Bitmap(myAssembly.GetManifestResourceStream(
myName_of_project + "." + "bread.JPG"));
//If it is necessary, we create the new buffer:
if (backBuffer == null)
{
backBuffer = new Bitmap(this.ClientSize.Width,
this.ClientSize.Height);
}
//We create the object of class Graphics from the buffer:
using (Graphics g = Graphics.FromImage(backBuffer))
{
//We clear the form:
g.Clear(Color.White);
//We draw the image in the backBuffer buffer:
g.DrawImage(breadImage, bx, by);
g.DrawImage(cheeseImage, cx, cy);
}
//We draw the image on the Form1:
e.Graphics.DrawImage(backBuffer, 0, 0);
//We turn on the timer:
timer1.Enabled = true;
} //End of the method Form1_Paint.
Если мы сейчас запустим программу на выполнение, то увидим, что мерцание уменьшилось, но не исчезло совсем. Это объясняется тем, что при выполнении метода Form1_Paint операционная система Windows сначала заполняет экран цветом фона (background color), в нашем примере белым фоном (white), и только после этого поверх фона прорисовывает встроенные в программу изображения. Поэтому необходимо сделать так, чтобы операционная система Windows не изменяла фон. Для этого воспользуемся неоднократно применяемым и в наших предыдущих книгах, и в данной книге шаблоном метода OnPaintBackground, в тело которого мы ничего не будем записывать, как показано на следующем листинге.
Листинг 4.11. Метод OnPaintBackground.
protected override void OnPaintBackground(
System.Windows.Forms.PaintEventArgs e)
{
//We prohibit to redraw a background.
}
Этот метод OnPaintBackground следует записать непосредственно за методом Form1_Paint, естественно, в теле класса Form1.
Теперь в режиме выполнения (Build, Build Selection; Debug, Start Without Debugging) подвижный сыр и неподвижный хлеб уже не мерцают, и мы решили данную задачу.
Однако при перемещении сыр может перекрыть батон хлеба (рис. 4.8), хотя по правилам игры пользователь должен управлять перемещением хлеба, не давая сыру упасть вниз, а маленький кусочек сыра при столкновении должен отскочить от большого батона хлеба в противоположном направлении. Поэтому методично и последовательно перейдём к решению этих задач.