Visual Basic, .NET, ASP, VBScript
 

   
 
Описание для автора не найдено
 
     
   
 

КАК Я РАЗРАБАТЫВАЛ ИГРУ "СТРЕЛОК 50"

 

Половый Александр

 

 

ПРОЛОГ

 

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

Поэтому меня можно отнести скорее к программистам-любителям, у которых есть E‑mail только на бесплатном сервере Polovy@mail.ru. Но, честно говоря, мне кажется, не этот признак должен определять любитель ты или профессионал.

В свободное от основной работы время я программирую "для души", для напряжения своего ума, для самовыражения, так сказать.

Часто бывает, что я берусь за программу для того, чтобы доказать (скорее самому себе), что на VB можно сделать все и даже больше. Я вижу чью-то разработку и говорю себе "я это сделаю на VB!". Например, так я поступил с "Виртуальной линейкой" (Ruler.exe), ее я сделал после того, как увидел программу Cool Ruler (www.fabsoft.com). Если Вам интересно, сравните их. У еще одной     моей программы RulerR.exe вообще аналогов нет, ведь она вращается!

Так вот игру "Стрелок 50" я задумал после того, как поиграл в одну из "стрелялок".

"Это просто" сказал я себе и принялся за работу.

 

 

"СТРЕЛЯЛКА" ПО ПРОИЗВОЛЬНО ДВИЖУЩИМСЯ ЦЕЛЯМ

 

Итак, что нам нужно? Прежде всего я определился, что моя программа работает в двух режимах:

1) Режим "ИГРА" FGamePlay = True, когда я как бешенный должен носиться за мишенями по всему окну и уничтожить как можно больше мишеней.

2) Режим "СТАНДАРТНАЯ ПРОГРАММА" FGamePlay = False, в котором моя игра выполняет функции любой другой программы: открывается,  закрывается, меняет свои размеры, может быть поверх остальных окон, включает/выключает звуки и сохраняет эти параметры в INI-файле.

 

Сначала поговорим о режиме "ИГРА".

 

 

СТРЕЛЬБА

 

Стрельба должна быть похожа на настоящую, для этого курсор мыши превращаем в перекрестие:

Me.MousePointer = 99

Me.MouseIcon = Image2.Picture

В Image2.Picture я поместил файл курсора в виде перекрестия. Дефицита в подобных файлах сейчас нет, а кое-что можно и самому нарисовать, например в ImageEdit.exe.

Выстрел должен звучать:

Dim PW As Integer

If MnuGameSound.Checked = True Then

PW = sndPlaySound(SW(1), SND_ASYNC Or SND_NODEFAULT)

End If

Флаг SND_ASYNC – прерывает предыдущий звук и проигрывает текущий

Флаг SND_NODEFAULT – не позволяет системе проигрывать звук по умолчанию, если не найден текущий звук

Путь к файлу со звуком выстрела 1.wav хранится в переменной SW(1):

SW(1) = App.Path & "\1.WAV"

Не забываем в Module1.bas указать функцию sndPlaySound:

Public Declare Function sndPlaySound Lib "winmm.dll" Alias "sndPlaySoundA" (ByVal lpszSoundName As String, ByVal uFlags As Long) As Long

Global Const SND_ASYNC = 1

Global Const SND_SYNC = 0

Global Const SND_NODEFAULT = &H2       

Если прицел наведен и выстрел сделан, то обязательно появится пулевое отверстие:

If Button = 1 Then

IShoot = IShoot + 1

Load Image4(IShoot)

Image4(IShoot).Picture = Image4(0).Picture

Image4(IShoot).Left = X - Image4(IShoot).Width / 2

Image4(IShoot).Top = Y - Image4(IShoot).Height / 2

Image4(IShoot).Visible = True

End If

Реагируем только на левую кнопку мыши Button = 1. С каждым выстрелом количество отверстий IShoot увеличивается на 1. Получается массив объектов Image4, "клонированных" из Image4(0).Picture.

Процессы создания отверстий помещаем в события MouseDown всех объктов, находящихся на форме – это сама форма, метки на ней, мишени да и сами картинки с пулевыми отверстиями.

Если событие произошло на мишени Image1 – то попадание:

Private Sub Image1_MouseDown(Index As Integer, Button As Integer, Shift As Integer, X As Single, Y As Single)

CurNShoots = CurNShoots + 1 'общее количество выстрелов

CurNTargetsTerm = CurNTargetsTerm + 1 'мишень унитожена

RatCurNTargetsTerm = RatCurNTargetsTerm + 1 * ScaleKoef 'стоимость мишени

FlagTargetTerm(Index) = True ' цель  уничтожена

End Sub

Флаг FlagTargetTerm показывает: уничтожена мишень или нет.

На остальных объектах – промах, например на форме:

Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)

CurNShoots = CurNShoots + 1 'общее количество выстрелов

CurNMiss = CurNMiss + 1 ' число промахов

RatCurNMiss = RatCurNMiss + 1 * ScaleKoef ' стоимость промаха

End Sub

Со временем на форме остается слишком много пулевых отверстий, поэтому периодически, скажем 1 раз в 3 секунды, проводим их "зачистку":

'------------------------------------------------------

'удалить пулевые отверстия

'------------------------------------------------------

Private Sub Timer4_Timer()

If FGamePlay = True Then

ClearHoles 'очищаем отверстия от пуль

Else ' игра окончена

Timer4.Enabled = False

End If

End Sub

Private Sub ClearHoles()

Dim I As Integer

For I = Image4.Count - 1 To 1 Step -1

Unload Image4(I)

Next I

IShoot = 0

End Sub

'------------------------------------------------------

'удалить пулевые отверстия

'------------------------------------------------------

 

 

МИШЕНИ

 

Стрелять мы научились, теперь "посоздаем" мишени. Пожалуй, это самое интересное в моей программе.

Чем любая игра похожа на жизнь? Непредсказуемостью.

За непредсказуемость у нас отвечает генератор случайных чисел в интервале от Nmin до Nmax:

Private Function NRnd(Nmin As Integer, Nmax As Integer) As Integer

Randomize

NRnd = Int((Nmax - Nmin + 1) * Rnd) + Nmin 'произвольный номер

End Function

и генератор случайного направления (знака "+" или "–") :

Private Function NSgnRnd() As Integer

Randomize

If Int(2 * Rnd) = 0 Then NSgnRnd = 1 Else NSgnRnd = -1

End Function

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

 

 

РОЖДЕНИЕ МИШЕНИ

 

Все мишени я отсортировал по типу движения TypeTarget:

Select Case TypeTarget(I) ' тип движения цели

Case 0 ' не подвижен

Case 1 'горизонтальное без смены направления

Case 2  'горизонтальное со сменой направления

Case 3 'вертикальное без смены направления

Case 4  'вертикальное со сменой направления

Case 5, 6  ' под любым углом без смены направления

Case 7, 8 ' под любым углом со сменой направления

Case 9 To 12 ' по окружности

End Select

Тип движения цели генерируется так:

TypeTarget(ITarget) = NRnd(0, 12) ' случайный тип движения цели

Чтобы управлять долей вероятности создания того или иного типа мишени, я на каждый тип отвел разное количество допустимых значений ITarget. Так для неподвижной мишени ITarget=0, а для двигающейся по окружности ITarget=9, 10, 11, 12, что означает вероятность возникновения последней мишени в 4 раза выше, чем первой, а этого мне и хотелось.

У каждой мишени есть свои характеристики: внешний вид, скорость (точнее шаг движения по горизонтали и вертикали, направление и т.п. (изменять размер мишеней я не захотел). Все эти характеристики присваиваются мишени в момент ее рождения:

Private Sub Timer2_Timer()

ITarget = Image1.Count 'новая цель

Load Image1(ITarget) ' загружаем цель

ReDim Preserve TypeTarget(1 To ITarget) 'создаем тип двжения

ReDim Preserve FlagTargetTerm(1 To ITarget)

ReDim Preserve LeftStepTarget(1 To ITarget) 'шаг по горизонтали

ReDim Preserve TopStepTarget(1 To ITarget) 'шаг по виртекали

' движение по окружности

ReDim Preserve AlfaTargetR(1 To ITarget)

ReDim Preserve AlfaStepTargetR(1 To ITarget)

ReDim Preserve RStepTargetR(1 To ITarget)

ReDim Preserve XrStepTargetR(1 To ITarget)

ReDim Preserve YrStepTargetR(1 To ITarget)

End Sub

Оператор ReDim изменяет размер массива, а параметр Preserve заставляет сохранять информацию, хранящуюся в массиве перед изменением.

Массивы задаются на уровне формы без задания размерности:

Dim TypeTarget() As Integer ' тип движения цели

Dim ITarget  As Integer ' количество целей

Dim FlagTargetTerm() As Boolean ' цель уничтожена?

'прямолинейное движение

Dim LeftStepTarget() As Single ' шаг перемещения

Dim TopStepTarget() As Single ' шаг перемещения

' движение по окружности

Dim AlfaTargetR() As Single ' угол перемещения

Dim AlfaStepTargetR() As Single ' шаг угла перемещения

Dim RStepTargetR() As Single ' шаг перемещения

Dim XrStepTargetR() As Single ' шаг перемещения

Dim YrStepTargetR() As Single ' шаг перемещения

Ну и разумеется, значения присваиваем этим характеристикам с максимальной непредсказуемостью (в пределах разумного конечно):

AlfaTargetR(ITarget) = NRnd(0, 360) / 60

AlfaStepTargetR(ITarget) = NRnd(5, 15) / 100 * NSgnRnd

RStepTargetR(ITarget) = (Me.ScaleWidth + Me.ScaleHeight) / 6 * NRnd(1, 100) / 100

XrStepTargetR(ITarget) = Me.ScaleWidth * NRnd(1, 100) / 100

YrStepTargetR(ITarget) = Me.ScaleHeight * NRnd(1, 100) / 100

'случайный шаг и направление

LeftStepTarget(ITarget) = NRnd(50, 150) * NSgnRnd

TopStepTarget(ITarget) = NRnd(50, 150) * NSgnRnd

FlagTargetTerm(ITarget) = False ' цель не уничтожена

'случайная картинка цели

I = NRnd(0, 9)

Image1(ITarget).Picture = Image3(I).Picture

'размеры

Image1(ITarget).Width = 32 * Screen.TwipsPerPixelX

Image1(ITarget).Height = 32 * Screen.TwipsPerPixelY

В качестве картинок я выбрал иконки старого доброго Windows (иногда так охота "расстрелять" его!).

Чтобы мишень была выше других объектов на форме и в нее можно было стрельнуть, ее надо "поднять":

Image1(ITarget).Visible = True

Image1(ITarget).ZOrder

Ну и чтобы было веселее, сделаем звук "рождения " мишени:

PW = sndPlaySound(SW(2), SND_ASYNC Or SND_NODEFAULT)

Для разнообразия управляем интервалами между созданиями мишеней, чтобы это было неравномерно:

'меняем интервал создания новой мишени от 0.1 до 1 сек

Timer2.Interval = NRnd(100, 1000)

Чтобы в игре одновременно существовало не более 10 мишеней я поставил условие:

If Image1.Count - 1 < 10 Then 'больше 10 одновременно нельзя

'Создание новой мишени

End If

Уничтоженные мишени на самом деле просто делаются невидимыми Visible=False (см. ниже), поэтому как только появляется такая "вакансия", старая убитая невидимая мишень становится новой видимой:

'используем удаленную мишень для новой

For I = 1 To Image1.Count - 1 ' обрабатываем все картинки

If Image1(I).Visible = False Then ' цель не видна

ITarget = I

Exit For

End If

Next I

Вот все это хозяйство оформляем в событии Timer2_Timer.

 

 

ДВИЖЕНИЕ МИШЕНИ

 

Я поставил интервал Timer1, как мне кажется, на самый короткий промежуток времени Timer1.Interval=50. Выше - медленно, ниже - система не успевает отработать.

В движении мишени ничего особенного нет, ну разве что движение по окружности прикольное. Если хотите добавьте сюда "тряску" мишени вокруг основной траектории с помощью того же Rnd, но я посчитал, что это слишком.

Private Sub Timer1_Timer()

If FGamePlay = True Then 'игра идет

For I = 1 To Image1.Count - 1 ' обрабатываем все картинки

If FlagTargetTerm(I) = False Then ' цель не поражена

Select Case TypeTarget(I) ' тип движения цели

Case 0 ' не подвижен

Case 1 'горизонтальное без смены направления

Image1(I).Left = Image1(I).Left + LeftStepTarget(I)

If Image1(I).Left > Me.ScaleWidth Then Image1(I).Left = 0

If Image1(I).Left < -Image1(I).Width Then Image1(I).Left = Me.ScaleWidth

If Image1(I).Top >= Me.ScaleHeight - Image1(I).Height Then Image1(I).Top = Me.ScaleHeight - Image1(I).Height

Case 7, 8 ' под любым углом со сменой направления

Image1(I).Left = Image1(I).Left + LeftStepTarget(I)

If Image1(I).Left > Me.ScaleWidth Then LeftStepTarget(I) = -LeftStepTarget(I)

If Image1(I).Left < -Image1(I).Width Then LeftStepTarget(I) = -LeftStepTarget(I)

Image1(I).Top = Image1(I).Top + TopStepTarget(I)

If Image1(I).Top > Me.ScaleHeight Then TopStepTarget(I) = -TopStepTarget(I)

If Image1(I).Top < -Image1(I).Height Then TopStepTarget(I) = -TopStepTarget(I)

Case 9 To 12 ' по окружности

AlfaTargetR(I) = AlfaTargetR(I) + AlfaStepTargetR(I)

Image1(I).Left = XrStepTargetR(I) + RStepTargetR(I) * Cos(AlfaTargetR(I))

Image1(I).Top = YrStepTargetR(I) + RStepTargetR(I) * Sin(AlfaTargetR(I))

End Select

End If

'-----------------------------------------------------

Next I

'-----------------------------------------------------

Else ' игра окончена

Timer1.Enabled = False

End If

End Sub

Это если цель не поражена, иначе делаем "гибель цели", например, сначала уменьшаем ее, а затем делаем невидимой:

Else 'цель поражена

If Image1(I).Visible = True Then

ImageWidth = Image1(I).Width - 5 * Screen.TwipsPerPixelX

ImageHeight = Image1(I).Height - 5 * Screen.TwipsPerPixelY

If ImageWidth < 0 Or ImageHeight < 0 Then

ImageWidth = 0

ImageHeight = 0

Image1(I).Visible = False

End If

Image1(I).Width = ImageWidth

Image1(I).Height = ImageHeight

End If

End If

Вот собственно и все: мишени летают, а мы по ним стреляем.

Осталось за немногим:

 

 

ПРАВИЛА ИГРЫ

 

Для разнообразия я ввел три варианта завершения игры FlagGameType (именно поэтому не просто "Стрелок", а "Стрелок  50"):

0) после совершения игроком 50 выстрелов с момента начала игры (не важно были попадания или промахи);

1) после уничтожения 50 мишеней (целей).

2) по истечении 50 секунд с момента начала игры;

За выстрелами следят все объекты, имеющиеся на форме во время игры, для формы выглядит это так:

Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)

'окончание игры

If FlagGameType = 0 And CurNShoots = MaxNShoots Then

MnuCancel1_Click

End If

End Sub

По аналогии не забываем про:

Image1_MouseDown – выстрел по мишени

Image4_MouseDown – выстрел по пулевому отверстию

Label1_MouseDown, Label2_MouseDown – выстрел по меткам с информацией

Из всех выстрелов попаданиями считаюся только Image1_MouseDown. Когда вы сгенерируете это событие 50 раз, игра закончится по сценарию 1.

'---------------------------------------------------

'окончание игры

If FlagGameType = 1 And CurNTargetsTerm = MaxNTargetsTerm … Then

MnuCancel1_Click

End If

End If

End If

'-----------------------------------------------------

За временем следит Timer3:

Private Sub Timer3_Timer()

If FGamePlay = True Then

CurNTimes = CurNTimes + 1

ITimes = ITimes + 1

If FlagGameType = 2 And CurNTimes = MaxNTimes Then

MnuCancel1_Click

Exit Sub

End If

Else ' игра окончена

Timer3.Enabled = False

End If

End Sub

Во всех трех случаях запускается некая MnuCancel1_Click, где и описывается завершение текущей игры:

Private Sub MnuCancel1_Click()

'-----------------------------------------------------

Dim PW As Integer 'звучание звукового файла

'-----------------------------------------------------

FGamePlay = False

Me.MousePointer = vbDefault

End Sub

Как только FGamePlay = False , все таймеры выключаются, а объекты не реагируют на щелчки мыши.

 

 

РЕЙТИНГ

 

Человек всегда сравнивает себя с другими и с самим собой.

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

Rating = (RatCurNTargetsTerm - RatCurNMiss) / CurNTimes * 1000

Итак, рейтинг равен разности между стоимостями попаданий и промахов поделенной на пройденное время (последующее умножение рейтинга на 1000 введено мною для получения "удобочитаемых" чисел Integer).

Для всех трех сценариев игры формула в целом справедлива.

Стоимости попаданий и промахов я придумал для учета размеров формы во время игры. Логика примерно такая: на форме меньшего размера мишени расположены кучнее и попасть в них легче, чем на форме большего размера, поэтому "стоимость" попадания на большой форме должна быть выше:

RatCurNTargetsTerm = RatCurNTargetsTerm+1 * ScaleKoef 'стоимость попадания

аналогично с промахом:

RatCurNMiss = RatCurNMiss  + 1 *  ScaleKoef 'стоимость попадания

Непосредственно масштабный коэффициент я рассчитал после нескольких десятков "битв" на формах с разными размерами. Его определение помещено в событии Form_Resize:

ScaleKoef = ((Me.Width ^ 2 + Me.Height ^ 2) ^ 0.5 / (MinMeWidth ^ 2 + MinMeHeight ^ 2) ^ 0.5) ^ (1 / 4)

В каждом событии, которое приводит к изменению рейтинга, запускается обновление информации для игрока:

Private Sub UpdateLabel()

If CurNTimes > 0 Then Rating = (RatCurNTargetsTerm - RatCurNMiss) / CurNTimes * ScaleKoef * 1000

Label2(0).Caption = CurNShoots

Label2(1).Caption = CurNTargetsTerm

Label2(2).Caption = CurNMiss

Label2(3).Caption = CurNTimes

Label2(4).Caption = Rating

End Sub

По завершении каждой игры обновляется таблица лидеров:

Private Sub UpdateLiders()

Dim I As Integer

Dim OldLiderRat As Integer

Dim OldLiderName As String

'---------------------------------------------------

For I = 10 To 1 Step -1

'---------------------------------------------------

If Rating > LiderRat(I) Then 'если рез больше следующего

CurPosition = I

OldLiderName = LiderName(I)

OldLiderRat = LiderRat(I)

LiderName(I) = CurName

LiderRat(I) = Rating

'---------------------------------------------------

If I < 10 Then

LiderName(I + 1) = OldLiderName

LiderRat(I + 1) = OldLiderRat

End If

'---------------------------------------------------

Else 'если результат меньше следующего

Exit For

End If

'---------------------------------------------------

Next I

'---------------------------------------------------

For I = 1 To 10

Text1(I).Text = LiderName(I)

Label4(I).Caption = LiderRat(I)

'---------------------------------------------------

If I = CurPosition Then 'красим в цвета

Text1(I).BackColor = vbGreen

Label4(I).BackColor = vbGreen

Label3(I).BackColor = vbGreen

Text1(I).Locked = False

Else

Text1(I).BackColor = vbWhite

Label4(I).BackColor = vbWhite

Label3(I).BackColor = vbWhite

Text1(I).Locked = True

End If

'---------------------------------------------------

Next I

'---------------------------------------------------

End Sub

Таким образом мы рассмотрели основные функции программы в режиме "Игра".

 

СТАНДАРТНАЯ ПРОГРАММА

 

Функции в этом режиме FGamePlay = False присущи практически всем программам.

Обратите внимание на возможность паузы во время игры. Чтобы вызватьь ее при нажатии кнопки Esc я использовал кнопку Command1, пряча ее при загрузке формы за ее пределы. Также пауза срабатывает при сворачивании формы Form_Resize, через 10 мин бездействия Timer3.timer и по правой кнопке мыши:

If Button = 2 Then 'правая кнопка

If FGamePlay = False Then

Me.PopupMenu mnuGame 'всплывающее меню

Else

Command1_Click 'пауза

End If

End If

Два слова о работе с INI-файлом.

Конечно, приведенный в программе вариант далеко не лучший, но он простой понятный и работает, к тому же я стараюсь минимально привлекать в программу dll‑библиотеки, но это, как говорится, мои проблемы.

Основная идея: читаем праметры через OpenINI, если файла нет запускаем параметры по умолчанию DefaultINI, приводим их в действие в LoadINI, сохраняем параметры в SaveINI перед выходом из программы.

Я использую такой вариант:

Private Sub Form_Unload(Cancel As Integer)

SaveINI

For I = Forms.Count - 1 To 0 Step -1

Unload Forms(I) 'выгружаются все формы программы

Next I

End Sub

С недавних пор в свои программы я всегда включаю процедуру:

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)

Unload Me

End Sub

Она позволяет корректно завершить работу программы (удаляются временные файлы и т.п.) при жестком закрытии программы, например, при выходе из текущего сеанса Windows.

 

Мне очень нравится решение одного из программистов (стыдно - не помню имени) по контролю минимально допустимой ширины и высоты формы при изменении ее размеров:

Private Sub Form_Resize()

If Me.Width < MinMeWidth Then

Me.Enabled = False

Me.Width = MinMeWidth

Me.Enabled = True

End If

If Me.Height < MinMeHeight Then

Me.Enabled = False

Me.Height = MinMeHeight

Me.Enabled = True

End If

End Sub

 

Функция "Поверх остальных окон" стала стандартной для современных программ.

'-----------------------------------------------------

'поверх остальных окон

'-----------------------------------------------------

Private Sub MnuGameForvard_Click()

If MnuGameForvard.Checked = False Then

MnuGameForvard.Checked = True

Else

MnuGameForvard.Checked = False

End If

MeForvard

End Sub

Private Sub MeForvard()

If MnuGameForvard.Checked = True Then

SetWindowPos Me.hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE

Else

SetWindowPos Me.hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE

End If

End Sub

'-----------------------------------------------------

'поверх остальных окон

'-----------------------------------------------------

И не забудте в Module1.bas:

'------------------------------------------

'всегда впереди

'------------------------------------------

Public Const HWND_TOPMOST = -1

Public Const HWND_NOTOPMOST = -2

Public Const SWP_NOACTIVATE = &H10

Public Const SWP_SHOWWINDOW = &H40

Public Const SWP_NOMOVE = &H2

Public Const SWP_NOSIZE = &H1

Declare Sub SetWindowPos Lib "user32" (ByVal hWnd As Long, ByVal hWndInsertAfter As Long, ByVal X As Long, ByVal Y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long)

'------------------------------------------

'всегда впереди

'------------------------------------------

Практически у всех моих программ используется одна и та же форма "О программе"  Form7. Так сказать "на поток" поставил.

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

 

Мир VB огромен и необъятен, поэтому закругляюсь.

 

 

ЭПИЛОГ

 

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

Совершенствуйте себя и свои программы!

Именно для этого я написал эту статью.

 

 

С уважением,

                          Половый Александр

Декабрь 2002 г.

 
     

   
   
     
  VBNet рекомендует