Идея устранения мерцания изображения методом двойной буферизации заключается в том, что сначала изображение проектируют не на экране, как до применения двойной буферизации, а в специальном буфере в памяти компьютера, а когда изображение полностью спроектировано в буфере памяти, оно копируется на экран . Так как процесс копирования готового изображения из буфера на экран происходит быстрее, чем процесс прорисовки изображения сразу на экране без использования промежуточного буфера, то мерцание изображения исчезает.
Чтобы устранить мерцание изображения при помощи двойной буферизации, приведённый выше код в теле метода Form1_Paint заменяем на тот, который дан на следующем листинге (с подробными комментариями).
Рис. 4.7. Подвижный сыр и неподвижный хлеб.
Рис. 4.8. Сыр закрывает хлеб.
Листинг 4.10. Метод для рисования изображения.
'Буфер в виде объекта класса Bitmap:
Dim backBuffer As Bitmap = Nothing
Private Sub Form1_Paint(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) _
Handles MyBase.Paint
'Загружаем в объект класса System.Drawing.Image
'добавленный в проект файл изображения заданного формата
'при помощи потока встроенного ресурса (ResourceStream):
cheeseImage = _
New Bitmap(myAssembly.GetManifestResourceStream( _
myName_of_project + "." + "cheese.JPG"))
breadImage = _
New Bitmap(myAssembly.GetManifestResourceStream( _
myName_of_project + "." + "bread.JPG"))
'Если необходимо, создаём новый буфер:
If (backBuffer Is Nothing) Then
backBuffer = New Bitmap(Me.ClientSize.Width, _
Me.ClientSize.Height)
End If
Using g As Graphics = Graphics.FromImage(backBuffer)
'Очищаем форму:
g.Clear(Color.White)
'Рисуем изображение в буфере backBuffer:
g.DrawImage(breadImage, bx, by)
g.DrawImage(cheeseImage, cx, cy)
End Using
'Рисуем изображение на форме Form1:
e.Graphics.DrawImage(backBuffer, 0, 0)
'Включаем таймер:
Timer1.Enabled = True
End Sub
Если мы сейчас запустим программу на выполнение, то увидим, что мерцание уменьшилось, но не исчезло совсем. Это объясняется тем, что при выполнении метода Form1_Paint операционная система Windows сначала заполняет экран цветом фона (background color), в нашем примере белым фоном (white), и только после этого поверх фона прорисовывает встроенные в программу изображения. Поэтому необходимо сделать так, чтобы операционная система Windows не изменяла фон. Для этого воспользуемся неоднократно применяемым и в наших предыдущих книгах, и в данной книге шаблоном метода OnPaintBackground, в тело которого мы ничего не будем записывать, как показано на следующем листинге.
Листинг 4.11. Метод OnPaintBackground.
Protected Overrides Sub OnPaintBackground( _
ByVal e As System.Windows.Forms.PaintEventArgs)
'Запрещаем перерисовывать фон (Background).
End Sub 'Конец метода OnPaintBackground.
Этот метод OnPaintBackground следует записать непосредственно за методом Form1_Paint, естественно, в теле класса Form1.
Теперь в режиме выполнения (Build, Build Selection; Debug, Start Without Debugging) подвижный сыр и неподвижный хлеб уже не мерцают, и мы решили данную задачу.
Однако при перемещении сыр может перекрыть батон хлеба (рис. 4.8), хотя по правилам игры пользователь должен управлять перемещением хлеба, не давая сыру упасть вниз, а маленький кусочек сыра при столкновении должен отскочить от большого батона хлеба в противоположном направлении. Поэтому методично и последовательно перейдём к решению этих задач.
Теперь программа должна перемещать батон хлеба таким образом, чтобы игрок мог отбивать хлебом сыр, как ракетка отбивает мяч в теннисе. Для перемещения объекта вверх (Up), вниз (Down), влево (Left) и вправо (Right) пользователь может использовать разнообразные элементы управления и компоненты с панели инструментов Toolbox, мышь, клавиатуру, джойстик и другие устройства. Для примера, размещаем на форме четыре кнопки Button с соответствующими заголовками в свойстве Text для перемещения хлеба Вверх, Вниз, Влево и Вправо (рис. 4.9). Перед размещением кнопок, для формы Form1 в панели Properties увеличиваем её размеры Size, например, до 384; 473.
По второму варианту, свяжем верхний левый угол прямоугольника, описанного вокруг хлеба, с указателем мыши, чтобы в режиме выполнения хлеб следовал за управляемым нами указателем мыши. В режиме проектирования дважды щёлкаем по каждой новой кнопке, а в панели Properties на вкладке Events дважды щёлкаем по имени события MouseMove. Появившиеся шаблоны методов для обработки этих событий после записи нашего кода принимают такой вид.
Рис. 4.9. Подвижный сыр и управляемый нами хлеб.
Рис. 4.10. Сыр закрывает хлеб.
Листинг 4.12. Методы для обработки событий.
Private Sub Button3_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button3.Click
'Перемещаем объект вверх:
by -= ySpeed
End Sub
Private Sub Button4_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button4.Click
'Перемещаем объект вниз:
by += ySpeed
End Sub
Private Sub Button5_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button5.Click
'Перемещаем объект влево:
bx -= xSpeed
End Sub
Private Sub Button6_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button6.Click
'Перемещаем объект вправо:
bx += xSpeed
End Sub
Private Sub Form1_MouseMove(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseMove
'Определяем координаты указателя мыши на форме:
Dim mouseX As Integer = e.X
Dim mouseY As Integer = e.Y
'Задаём координаты хлеба, равные координатам мыши:
bx = mouseX
by = mouseY
End Sub
Для удобства, задаём другие значения начальным координатам хлеба (например,
Dim bx As Integer = 150 : Dim by As Integer = 200)
в нижней части формы.
После запуска программы (Build, Build Selection; Debug, Start Without Debugging) сыр самостоятельно (без нашего участия) перемещается по экрану, отталкиваясь от границы со звуковым сигналом.
А после нажатий мышью четырёх кнопок Вверх, Вниз, Влево и Вправо мы можем перемещать батон хлеба в соответствующих четырёх направлениях по всему экрану (рис. 4.9).
На рис. 4.9 видно, что по умолчанию на форме выделена первая кнопка Button (на этой кнопке размещён фокус (Focus) программы), и поэтому данную кнопку мы можем нажать не только мышью, но и клавишей Enter, после чего скорость перемещения сыра возрастёт пропорционально количеству нажатий. Если мы желаем, чтобы по умолчанию была выделена другая, например, вторая кнопка Button, то в каком-либо методе, например, в методе Form1_Paint следует записать известный код (button2.Focus();). В режиме выполнения мы можем перемещать фокус, например, клавишами со стрелками или Tab на любую кнопку на форме, после чего воздействовать на эту кнопку клавишей Enter.
По второму варианту, мы связали верхний левый угол прямоугольника, описанного вокруг хлеба, с указателем мыши, и теперь в режиме выполнения хлеб следует за управляемым нами указателем мыши.
Следовательно, мы решили задачу по управлению объектом (в виде батона хлеба) при помощи элементов управления, например, кнопок Button и мыши.
Аналогично, как для события MouseMove, для управления игрой можно использовать методы-обработчики других событий мыши, которые имеются в панели Properties на вкладке Events для формы Form1 (рис. 4.11).
Рис. 4.11. События для управления игрой.
Отметим, что для управления игрой вместо кнопок Button (чтобы не загромождать форму) и мыши можно использовать и клавиши клавиатуры по описанной далее методике.
Однако при перемещении сыр может перекрыть батон хлеба (рис. 4.10), хотя по правилам игры, напомним, пользователь должен управлять перемещением хлеба, не давая сыру упасть вниз, а маленький кусочек сыра при столкновении должен отскочить от большого батона хлеба в противоположном направлении. Поэтому перейдём к решению задачи о столкновении летающих объектов (в следующей главе).
Таким образом, в этой главе мы разработали такие общие методики:
добавления объекта в проект;
анимации объекта;
проектирования отскока объекта от заданной нами границы, например, экрана;
управления скоростью перемещения объекта;
добавления звукового сигнала в ключевые для игры моменты, например, в момент столкновения объекта с границей;
добавления нового объекта в игру (с использованием общего для всех объектов кода);
устранения мерцания изображения при помощи двойной буферизации;
управления направлением перемещения объекта с помощью клавиш.
Эти методики можно использовать при разработке самых разнообразных игр.
Продолжаем разработку методики создания типичной и широко распространённой игры, когда в качестве летающих игровых объектов используются продукты питания, следуя следующей статье с сайта microsoft.com: Rob Miles. Games Programming with Cheese: Part Two. Так как в данной статье программы написаны на языке Visual C# и, кроме того, для устаревшей версии Visual Studio, то автору данной книги пришлось переписать все программы на язык Visual Basic и, кроме того, для современной версии Visual Studio. Общие требования к программному обеспечению для разработки этой игры приведены выше.
Также продолжаем методично и последовательно решать типичные задачи по созданию данной и всех подобных игр.
Программы игр могут обнаружить столкновения между объектами при помощи прямоугольников, описанных вокруг заданных объектов. Естественно, это является существенным допущением, т.к. подавляющее большинство объектов имеют форму, отличную от прямоугольника. Однако данное допущение применяется во многих играх, и пользователь в азарте игры не замечает этой погрешности.
Прямоугольник, описанный вокруг изображения батона хлеба bread.jpg, показан на рис. 5.1.
Рис. 5.1. Прямоугольник, описанный вокруг хлеба.
Ширина полей между объектом и описанным вокруг объекта прямоугольником должна быть сведена к минимуму, чтобы объект обязательно касался прямоугольника в как можно большем количестве точек и отрезков линий. Если начало прямоугольной системы координат “x, y” находится в верхнем левом углу экрана , то координаты верхней левой точки (bx, by) и нижней правой точки (bx + batWidth, by + batHeight) однозначно определяют данный прямоугольник на экране.
В среде выполнения .NET Framework (для настольных компьютеров) известна структура Rectangle (из пространства имён System.Drawing), у которой метод-конструктор Rectangle Constructor имеет несколько перегрузок. Наиболее применяемая перегрузка метода-конструктора Rectangle Constructor (которую далее и мы будем часто применять) с параметрами (Int32, Int32, Int32, Int32) структуры Rectangle на главных (в мире программирования) языках приведена в табл. 5.1.
Таблица 5.1.
Метод-конструктор Rectangle Constructor (Int32, Int32, Int32, Int32) структуры Rectangle.
Visual Basic (Declaration)
Public Sub New ( _
x As Integer, _
y As Integer, _
width As Integer, _
height As Integer _
)
Visual Basic (Usage)
Dim x As Integer
Dim y As Integer
Dim width As Integer
Dim height As Integer
Dim instance As New Rectangle(x, y, width, height)
C#
public Rectangle (
int x,
int y,
int width,
int height
)
C++
public:
Rectangle (
int x,
int y,
int width,
int height
)
J#
public Rectangle (
int x,
int y,
int width,
int height
)
JScript
public function Rectangle (
x : int,
y : int,
width : int,
height : int
)
В этом определении метода-конструктора Rectangle Constructor параметры переводятся так:
x – координата “x” верхнего левого угла прямоугольника;
y – координата “y” верхнего левого угла прямоугольника;
width – ширина (по оси “x”) прямоугольника;
height – высота (по оси “y”) прямоугольника.
Далее в нашей программе мы сначала объявим прямоугольники, описанные вокруг объектов, как новые переменные, например, так:
'Прямоугольник, описанный вокруг первого объекта:
Dim cheeseRectangle As Rectangle
'Прямоугольник, описанный вокруг второго объекта:
Dim breadRectangle As Rectangle
а затем в каком-либо методе создадим (при помощи ключевого слова new) и инициализируем эти объекты-прямоугольники, например, так:
cheeseRectangle = New Rectangle(cx, cy, _
cheeseImage.Width, cheeseImage.Height)
breadRectangle = New Rectangle(bx, by, _
breadImage.Width, breadImage.Height)
В этой структуре Rectangle (из пространства имён System.Drawing) имеются методы, которые могут обнаруживать пересечения различных перемещающихся прямоугольников. Эти методы определяют, находится ли точка одного прямоугольника внутри другого прямоугольника, и если находится, то программа определяет эту ситуацию и как столкновение этих двух прямоугольников, и как столкновение двух объектов, расположенных внутри этих прямоугольников.
Когда далее при написании программы мы поставим оператор-точку “.” после какого-либо объекта структуры Rectangle, то увидим подсказку с двумя основными методами Intersect и IntersectsWith (рис. 5.2) для обнаружения пересечения двух прямоугольников.
Рис. 5.2. Подсказка с методами Intersect и IntersectsWith.
Определение для наиболее применяемого метода IntersectsWith (который далее и мы будем часто применять) с параметром (Rectangle rect) структуры Rectangle на главных (в мире программирования) языках приведено в табл. 5.2.
Таблица 5.2.
Определение метода Rectangle.IntersectsWith структуры Rectangle.
Visual Basic (Declaration)
Public Function IntersectsWith ( _
rect As Rectangle _
) As Boolean
Visual Basic (Usage)
Dim instance As Rectangle
Dim rect As Rectangle
Dim returnValue As Boolean
returnValue = instance.IntersectsWith(rect)
C#
public bool IntersectsWith (
Rectangle rect
)
C++
public:
bool IntersectsWith (
Rectangle rect
)
J#
public boolean IntersectsWith (
Rectangle rect
)
JScript
public function IntersectsWith (
rect : Rectangle
) : Boolean
Этот метод IntersectsWith обнаруживает пересечение заданного нами первого прямоугольника со вторым прямоугольником, объявленного здесь как параметр rect.
Если метод определит, что ни одна точка одного прямоугольника не находится внутри другого прямоугольника, то метод возвращает логическое значение False.
А если метод определит, что хотя бы одна точка одного прямоугольника находится внутри другого прямоугольника, то метод IntersectsWith возвращает логическое значение True, и это значение применяется для изменения направления движения какого-либо прямоугольника на противоположное (чтобы уйти от дальнейшего пересечения), например, в таком коде:
'Проверяем столкновение объектов:
If (cheeseRectangle.IntersectsWith(breadRectangle)) Then
'Изменяем направление движения на противоположное:
goingDown = Not goingDown
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
Теперь в проекте, который мы начали разрабатывать в предыдущей главе (и продолжаем в данной главе) объявляем два прямоугольника, а приведённый выше код в теле метода Form1_Paint заменяем на тот, который дан на следующем листинге (с подробными комментариями).
Листинг 5.1. Метод для рисования изображения.
'Прямоугольник, описанный вокруг первого объекта:
Dim cheeseRectangle As Rectangle
'Прямоугольник, описанный вокруг второго объекта:
Dim breadRectangle As Rectangle
Private Sub Form1_Paint(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) _
Handles MyBase.Paint
'Загружаем в объекты класса System.Drawing.Image
'добавленные в проект файлы изображения заданного формата
'при помощи потока встроенного ресурса (ResourceStream):
cheeseImage = _
New Bitmap(myAssembly.GetManifestResourceStream( _
myName_of_project + "." + "cheese.JPG"))
breadImage = _
New Bitmap(myAssembly.GetManifestResourceStream( _
myName_of_project + "." + "bread.JPG"))
'Инициализируем прямоугольники, описанные вокруг объектов:
cheeseRectangle = New Rectangle(cx, cy, _
cheeseImage.Width, cheeseImage.Height)
breadRectangle = New Rectangle(bx, by, _
breadImage.Width, breadImage.Height)
'Если необходимо, создаём новый буфер:
If (backBuffer Is Nothing) Then
backBuffer = New Bitmap(Me.ClientSize.Width, _
Me.ClientSize.Height)
End If
'Создаём объект класса Graphics из буфера:
Using g As Graphics = Graphics.FromImage(backBuffer)
'Очищаем форму:
g.Clear(Color.White)
'Рисуем изображение в буфере backBuffer:
g.DrawImage(cheeseImage, cx, cy)
g.DrawImage(breadImage, bx, by)
End Using
'Рисуем изображение на форме Form1:
e.Graphics.DrawImage(backBuffer, 0, 0)
'Включаем таймер:
Timer1.Enabled = True
End Sub
А вместо приведённого выше метода updatePositions для изменения координат записываем следующий метод, дополненный кодом для обнаружения столкновения объектов.
Листинг 5.2. Метод для изменения координат и обнаружения столкновения объектов.
Sub updatePositions()
If (goingRight) Then
cx += xSpeed
Else
cx -= xSpeed
End If
If ((cx + cheeseImage.Width) >= Me.ClientSize.Width) Then
goingRight = False
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
If (cx <= 0) Then
goingRight = True
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
If (goingDown) Then
cy += ySpeed
Else
cy -= ySpeed
End If
If ((cy + cheeseImage.Height) >= Me.ClientSize.Height) Then
goingDown = False
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
If (cy <= 0) Then
goingDown = True
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
'Задаём прямоугольникам координаты объектов:
cheeseRectangle.X = cx
cheeseRectangle.Y = cy
breadRectangle.X = bx
breadRectangle.Y = by
'Проверяем столкновение объектов:
If (cheeseRectangle.IntersectsWith(breadRectangle)) Then
'Изменяем направление движения на противоположное:
goingDown = Not goingDown
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
End Sub
В режиме выполнения (Build, Build Selection; Debug, Start Without Debugging) при помощи кнопок и мыши мы можем перемещать хлеб и этим хлебом, как ракеткой, отбивать сыр или вверх, или вниз (рис. 5.3). Напомним, что, так как угол падения сыра на хлеб равен 45 градусам, то и угол отражения сыра от хлеба (и от границ экрана) также равен 45 градусам.
Приведённый на предыдущем листинге код обнаруживает столкновение только тогда, когда сыр падает на хлеб сверху вниз и соприкасается с верхней плоскостью хлеба. Если же сыр соприкасается с хлебом сбоку (слева или справа), то отскока сыра от хлеба не происходит. Поэтому устраним этот недостаток, чтобы игра была более реалистичной.
Если мы оперируем с окружностями, описанными вокруг объектов, то возможны три основные схемы столкновений, показанные на рис. 5.4. В схемах 1 и 3 маленький круг ударяется о большой круг под углом 45 градусов и отражается под этим же углом и по этой же линии. В схеме 2 маленький круг ударяется о большой круг под углом 90 градусов и также вертикально отражается вверх.
Если же мы оперируем с прямоугольниками, описанными вокруг объектов, то возможны четыре основные схемы столкновений, показанные на рис. 5.5.
Рис. 5.3. Сыр отскочил от хлеба.
Рис. 5.4. Три схемы столкновений.Рис. 5.5. Четыре схемы столкновений.
В схемах 1 и 4 маленький прямоугольник ударяется о большой прямоугольник сбоку под углом 45 градусов и отражается под этим же углом и по этой же линии. В схемах 2 и 3 маленький прямоугольник падает на большой прямоугольник под углом 45 градусов, но отражается не по линии падения, а по линии отражения, перпендикулярной линии падения.
Для реализации более правильных схем столкновений, показанных на рис. 5.5, в нашем проекте вместо приведённого выше метода updatePositions для изменения координат записываем следующий метод, дополненный новым кодом для обнаружения столкновения объектов.
Листинг 5.3. Метод для изменения координат и обнаружения столкновения объектов.
Sub updatePositions()
If (goingRight) Then
cx += xSpeed
Else
cx -= xSpeed
End If
If ((cx + cheeseImage.Width) >= Me.ClientSize.Width) Then
goingRight = False
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
If (cx <= 0) Then
goingRight = True
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
If (goingDown) Then
cy += ySpeed
Else
cy -= ySpeed
End If
If ((cy + cheeseImage.Height) >= Me.ClientSize.Height) Then
goingDown = False
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
If (cy <= 0) Then
goingDown = True
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
End If
'Проверяем столкновение объектов:
If (goingDown) Then
'Если сыр движется вниз и имеется столкновение:
If (cheeseRectangle.IntersectsWith(breadRectangle)) Then
'В момент столкновения подаем звуковой сигнал Beep:
Beep()
'мы имеем столкновение:
Dim rightIn As Boolean = breadRectangle.Contains( _
cheeseRectangle.Right, _
cheeseRectangle.Bottom)
Dim leftIn As Boolean = breadRectangle.Contains( _
cheeseRectangle.Left, _
cheeseRectangle.Bottom)
'виды столкновений:
If (rightIn And leftIn) Then
'отскок вверх:
goingDown = False
Else
'отскок вверх:
goingDown = False
'отскоки по горизонтали:
If (rightIn) Then
goingRight = False
End If
If (leftIn) Then
goingRight = True
End If
End If
End If
End If
End Sub
В режиме выполнения (Build, Build Selection; Debug, Start Without Debugging) при помощи кнопок Button и мыши мы можем перемещать хлеб и этим хлебом, как ракеткой, отбивать сыр вверх не только верхней стороной прямоугольника (описанного вокруг объекта), как было в предыдущем коде, но теперь и боковыми сторонами этого прямоугольника. Однако мы можем отбивать, только если сыр перемещается сверху вниз.