Часть2_Visual Basic.Net - Игровой цикл в 1000/сек на API таймере

API таймер Часть 2
А теперь мы подробнее разберем дополнительные возможности программирования
с использованием API таймера

Первая часть статьи находиться здесь

[spoiler]
Здесь будет рассмотрено подключение одновременно двух и даже трех таких таймеров,
где можно будет задавать различные интервалы для каждого.
А также менять интервалы этого специфичного апи таймера в реальном времени,
то есть, сама программа, уже скомпилированная,
может менять интервал, частоту срабатывания апи таймера

Сейчас я приведу код, который очень просто подключает два таймера.
Один будет срабатывать каждую 1 миллисекунду, а другой 10 миллисекунд

По сравнению с кодом, который был в первой части, здесь будет добавлено
всего несколько строчек кода к нему, который выполнит нашу поставленную выше цель.
Скопируйте весь код, приведенной в первой части.

Измените название подпрограммы на gameloop1, в котором будет крутиться код.
Там у него было название apitimer1.

Public Sub gameloop1()

End Sub


Вы можете сами называть имя, как хотите, но я стараюсь, чтобы вы быстрее освоили
суть.

Обращение к этой подпрограмме происходит по имени этой подпрограммы.
То есть, в нашем конкретном случае gameloop1.

Поэтому также придется изменить прежнюю строчку кода

Dim timer As myCallback = AddressOf apitimer1

На это

Dim time1 As myCallback = AddressOf gameloop1

И также, изменить имя переменной timer на time1.
Не на timer1, а ту будет путаница для компилятора.

Обозначение AddressOf как бы перенаправляет сообщения апи функции
на подпрограмму gameloop1.

Каждый раз, когда срабатывает таймер timeSetEvent в библиотеке windows
winmm.dll, он каждый раз адресует сообщения на gameloop1.

Но таймер еще не работает, нужно его создать.
А создается он такой строчкой.

timerID1 = timeSetEvent(1, 0, time1, strnull, elapse_Periodically)

Этот код можно помещать там, где именно вы хотите создать таймер, я
для простоты поместил его в Form1_Load.
timeSetEvent состоит из четырех параметров.
- Нас интересуют первый и третий параметр.
В первом параметре, где мы поставили один, означает, что таймер
будет срабатывать с частотой 1 миллисекунд.
Он принимает значание integer, на заметку<, и не может ставиться меньше одного.

В третьем параметре указывается имя переменной time1.
Его имя должно соответствовать созданной ранее переменной типа myCallback

Dim time1 As myCallback = AddressOf gameloop1



А строчкой

timeKillEvent(timerID1)

Вы уничтожаете таймер timerID1. timerID1 несет в себе иденфикационный номер,
по которому идет обращение системы windows. Эту строку вы тоже можете
поместить там где хотите. Я изменил для краткости, поместив его в Form1_FormClosed
А был он в первой части в обработчике клавиатуры.

Ну, что теперь, давайте сделаем то, что хотели, а именно подключим два таймера
с разными интервалами.

Это делается легко добавляем такую строку.

Dim time2 As myCallback = AddressOf gameloop2


В итоге всё это будет выглядеть таким образом.

Dim time1 As myCallback = AddressOf gameloop1
Dim time2 As myCallback = AddressOf gameloop2


Создаем вторую аналогичную подпрограмму с именем,
как вы думаете?


Public Sub gameloop2()

End Sub


И нужно создать второй таймер. Создадим его в Form1_Load.
В итоге это все будет выглядеть вот так.

timerID1 = timeSetEvent(1, 0, time1, strnull, elapse_Periodically)
timerID1 = timeSetEvent(10, 0, time2, strnull, elapse_Periodically)


Привожу полный код

/*http://esate.ru, gally*/

Public Class Form1


   Dim time1 As myCallback = AddressOf gameloop1
   Dim time2 As myCallback = AddressOf gameloop2

   Public Declare Function timeKillEvent Lib "winmm.dll" (ByVal uID As Integer) As Integer
   Public Declare Function timeSetEvent Lib "winmm.dll" (ByVal interval_Ms As Integer, ByVal resolution_Ms As Integer, ByVal lpFunction As [Delegate], ByVal dwUser As Integer, ByVal uFlags As Integer) As Integer
   Public Delegate Sub myCallback(ByVal uID As Integer, ByVal uMsg As Integer, ByVal dwUser As Integer, ByVal dw1 As Integer, ByVal dw2 As Integer)

   
   Dim timerID1 As Integer

   Public Const elapse_Periodically As Integer = 1
   Const strnull As String = Nothing


   Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

      timerID1 = timeSetEvent(1, 0, time1, strnull, elapse_Periodically)
      timerID1 = timeSetEvent(10, 0, time2, strnull, elapse_Periodically)

   End Sub


   Private Sub Form1_FormClosed(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed

      timeKillEvent(timerID1)

      End
   End Sub


   Public Sub gameloop1()

   End Sub


   Public Sub gameloop2()

   End Sub


Получилось! Один срабатывает 1000 раз в секунду, другой 100 раз в секунду.

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

Когда был один таймер, чаще всего возникали поддергивания экрана, так как
всё это было завязано на одном таймере. Здесь у вас появляется некоторая свобода.
В одном, вы можете просто крутить графику, а в другом обрабатывать все остальное.
Можете крутить графику не с частотой 100, а 200, а можете и 60/сек!

Ещё дополнительные ньюансы!
Вы можете увидеть в коде, что строчка

timeKillEvent(timerID1)

Так и осталась одна. Что это означает в нашем случае?

Если посмотреть на код, находящейся в Form1_Load, вы можете догадаться.

timerID1 = timeSetEvent(1, 0, time1, strnull, elapse_Periodically)
timerID1 = timeSetEvent(10, 0, time2, strnull, elapse_Periodically)


В этом конкретном случае это просто означает строкой
timeKillEvent(timerID1)
остановку сразу двух таймеров!

Но вдруг возникнет такая ситуация, что нам нужно остановить только один из них?

Делается это тоже очень просто!

Добавьте, где происходят глобальные инициализации переменных, к строке

Dim timerID1 As Integer

дополнительную строку

Dim timerID2 As Integer

И в Form1_Load измените код

timerID1 = timeSetEvent(1, 0, time1, strnull, elapse_Periodically)
timerID1 = timeSetEvent(10, 0, time2, strnull, elapse_Periodically)


На

timerID1 = timeSetEvent(1, 0, time1, strnull, elapse_Periodically)
timerID2 = timeSetEvent(10, 0, time2, strnull, elapse_Periodically)


Все! Теперь у вас появляется выбор!
Система идентифицировала, что вы подключили уже два апи таймера к вашей
программе. А в прошлом случае был один! Только сообщения разделялись
по-разному, с разными интервалами!
Можете остановить первый таким образом

timeKillEvent(timerID1)

А второй

timeKillEvent(timerID2)

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

timerID1 = timeSetEvent(5, 0, time1, strnull, elapse_Periodically)

Но не получиться!

А получиться таким образом


timeKillEvent(timerID1)
timerID1 = timeSetEvent(5, 0, time1, strnull, elapse_Periodically)


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


На заметку: некоторые специально привязывают fps к частоте обновления монитора,
считая что это дополнительно гарантирует плавное отображение графики!

Для справки:
Вы также можете средствами TaoFramework например, узнать частоту обновления
монитора. У некоторых, это может быть 60, чаще 75, бывает и 85,
у особенных мониторов и 100.

И помещать такой код для графики

timerID1 = timeSetEvent(1000/Частота_обновления_монитора, 0, time1, strnull, elapse_Periodically)


Но здесь ещё можно дать совет!Дело в том, что при создании двух таймеров, и даже трех таймеров,
они, таймеры, ни такие уж и независимые друг от друга!

Например, если окажется так, что в первой процедуре

Public Sub gameloop1()

End Sub



Код будет не успевать за таймером, то это скажется и на второй процедуре


Public Sub gameloop2()

End Sub


Замедление в первой, а во второй?! Но, если вы хотите избежать этого, то поместите
код, у которого высока вероятность большой нагрузки,
не в первую процедуру gameloop1(), а во вторую gameloop2().
Даже если gameloop2() и зависнет по-страшному, gameloop1()
будет скакать, как и скакал!
Но если этот же тяжелый код будет стоять в gameloop1(),
то он будет работать медленно, не в нежелаемый изначальна такт,
а вот gameloop2(), и если есть gameloop3(),
вообще не будут никак тактировать!
И ещё мой личный совет, создавайте все-таки всегда timerID2,
timerID3, если нужно. Больше независимости друг на друга. ИМХО.

Другой вопрос. А не многовато ли для игрового темпа целых 1000 тактов?!
Ответ: Я не скажу строго определенно, но мне кажется лучше 1000, игровой темп будет
более плавным, так как темп игры будет разбит на малых приращениях, а у
этого апи таймера все-таки есть маленькие погрешности. ИМХО!

Возможно, есть у некоторых такой вопрос! Вот я решил сделать игровой цикл
в 1000 раз в секунду, а графику в 60. И я все сделал правильно! Конкретно,
игровой цикл, я расцениваю, как вполне предсказуемым по нагрузке и
помещаю его в gameloop1(), а графику, я оцениваю как тяжелую, непредсказуемую,
мне там тормоза не нужны, и я это всё помещаю в gameloop2().
Вот, что если у меня графика зависнет на 22 кадров, в то время как идет игровой
цикл с 1000/сек, а я ведь настроил таймер для графики на 1000/текущая_частота_обновления_экрана.
Может ли случиться так, что графика не успеет нормально до конца отрисоваться, как
уже пошел следующий такт от таймера, а он все-таки же невстроенный, а API???

Ответ: Графика будет до конца отрисововаться, так как процедура
не может начаться вновь, пока не завершиться, "не дойдет до конца End Sub".
Это значит, что автоматически, когда нагрузка графики станет в норме, вновь
поднимется до 60 FPS.

Здесь иногда удобно. Пользователю нет сильной разницы между 200 FPS и 100 FPS.
Но в тоже время есть больше необходимость сохранять игровой темп,
что мы и реализовали!

Но бывает и такой случай, когда наступает страшный тормоз! Как быть?
FPS = 4, игровой темп под 1000, А-а-а. Изображения, так скажем,
не плавно едят, а стали сильно подпрыгивать, и долго стоять на месте!!!
Сделать игровой цикл с меньшим тактированием, так как скорость
игры возникла по части графики, а не по части ИИ. Ну что уж поделать!
Но бывает и другой или дополнительный случай, когда есть возможность
отключить на ходу некоторые суперэффекты,
когда например FPS падает ниже 24. Например, если есть
анизотропная фильтрация, то в случае необходимости, перевести ее с 8 до 4!
Например здесь, я упомянул наличие возможностей обхода каллапса FPS, но думаю
игродел знает лучше, как это сделать со своей игрушкой!

И с помощью чего, можно решить эти две проблемы!

Есть несколько решений

У Visual Basic.Net есть неплохой счетчик. Называется StopWatch
Это не таймер, а счетчик. Им можно замерять прошедшие такты, миллисекунды,
от начала его включения. Я сам часто его использую для измерения
скорости кода.
Но в этом конкретном контексте есть простое решение!

То есть вы наверное догадались, что нужно что-то, которое вносила бы
коррекцию по части циклов.

Кидайте на форму элемент управления таймер.net - элемент управления
и TextBox, выбирите его свойства, где Enabled справа сделайте true,
И потом напишите интервал в 1000.

Делается это просто! Объявляется глобальная переменная

Dim fps As Integer



Дальше прописываете в процедуре gameloop2() такой код

Public Sub gameloop2()

fps = fps + 1
End Sub


На форме кликаете на Timer1 и вводите такой код

Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick

TextBox1.Text = "FPS = " + fps.ToString + " кадров/сек"
fps = 0

End Sub


Для наглядности я показал вам, как вы можете считать количество кадров.

Но возникает сомнение, а как же я буду точно корректировать, чтобы
справляться с перегрузкой, если измеряю
только с разностью одной секунды?!

Например, можно так сделать.

Делаете интервал таймера.net на 200
А в обработчике таймера пишите такой код

Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick


fps = fps * 5
TextBox1.Text = "FPS = " + fps.ToString + " кадров/сек"
fps = 0

End Sub




Почему три таймера могут быть полезны, а не два?
Потому что, бывает опрашивать клавиатуру и мышь 1000 раз бывает излишним.
Но бывает и наоборот, это удобно!
Зависит от жанра игры.
Но для справки с частотой 100 в секунду в большинстве вполне нормально!

Киньте два элемента Textbox и Timer.net и задайте ему интервал в 1000.
И не забудьте включить этот таймер.
Вот такой код

/*http://esate.ru, gally*/

Public Class Form1

   Dim x As Integer

   Dim fps As Integer
   Dim nagruzka As Integer

   Dim time1 As myCallback = AddressOf gameloop1
   Dim time2 As myCallback = AddressOf gameloop2
   Dim time3 As myCallback = AddressOf gameloop3

   Public Declare Function timeKillEvent Lib "winmm.dll" (ByVal uID As Integer) As Integer
   Public Declare Function timeSetEvent Lib "winmm.dll" (ByVal interval_Ms As Integer, ByVal resolution_Ms As Integer, ByVal lpFunction As [Delegate], ByVal dwUser As Integer, ByVal uFlags As Integer) As Integer
   Public Delegate Sub myCallback(ByVal uID As Integer, ByVal uMsg As Integer, ByVal dwUser As Integer, ByVal dw1 As Integer, ByVal dw2 As Integer)


   Dim timerID1 As Integer
   Dim timerID2 As Integer
   Dim timerID3 As Integer

   Public Const elapse_Periodically As Integer = 1
   Const strnull As String = Nothing


   Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

      TextBox2.Text = 400000

      timerID1 = timeSetEvent(1, 0, time1, strnull, elapse_Periodically)
      timerID2 = timeSetEvent(10, 0, time2, strnull, elapse_Periodically)
      timerID3 = timeSetEvent(1000/75, 0, time3, strnull, elapse_Periodically)
   End Sub


   Private Sub Form1_FormClosed(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed
      
      timeKillEvent(timerID1)
      timeKillEvent(timerID2)
      timeKillEvent(timerID3)
      End
   End Sub


   Public Sub gameloop1()

   End Sub


   Public Sub gameloop2()

   End Sub


   Public Sub gameloop3()

      Do Until x <= nagruzka
        If x < nagruzka Then
           x = x + 1
        End If
      Loop
      x = 0

      fps = fps + 1
   End Sub


   Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick

      nagruzka = TextBox1.Text
      TextBox2.Text = "FPS = " + fps.ToString + " кадров/сек"

      fps = 0
   End Sub

End Class


Я сделал возможность имитировать загруженность через цикл.
Можете попробовать увеличивать число в Textbox-е и
смотреть примитивное "FPS". Для наглядности все это!

И также там, в обработчике таймера.нет можно корректировать
частоту циклов, если что. Как это делать я показал,
с помощью двух строк, с использованием timeKillEvent.
А также, если посчитаете нужным, отключить некритичные красивости
графики.
Примерно это выглядило бы так.

If fps < 20 Then
'справляетесь с зависоном
End If


Хочу отмететь
Что не все ньюансы этого апи таймера я знаю!
Но я указал те места, на которые сам споткнулся.
Например, совершенно точно, что там вы не сможете
крутить код вместе с такими элементами управления, как
PictureBox, TextBox, Label.

Наконец дописал!
Если вы хотите воспринимать данную статью, как основную часть
2D или 3D движка, то пожалуйста!
</cut>
0      2202        06.08.2011

^
Регистрация
Регистрируясь, вы принимаете правила сайта. Если вы не получили код подтв. регистрации - не забудьте проверить папку спам.
Логин*
Email*
Пароль*
Подтверждение пароля*
 
Логин*
Код*
 
×
Восстановление пароля
Пожалуйста, заполните поля, после чего вы получите код подтверждения на ваш Email. Если код не пришел в течении нескольких минут - проверьте папку спам.
Логин

или Email
Логин*
Код подтверждения*
Новый пароль*
Подтверждение пароля*
×
Авторизация
  • Используйте вашу учетную запись на Facebook.com для входа на сайт.
  • Используйте вашу учетную запись VKontakte для входа на сайт.
  • Используйте вашу учетную запись Google для входа на сайт.
Авторизуйтесь с помощью соц. сети или с помощью аккаунта на сайте:
×