Эту статью я написал около месяца назад. Ориентирована она на полных новичков.
До этого выкладывал её только на vladgamedev.com (маленький форум игроделов Владивостока), теперь решил выложить и сюда.
*********
Делаем простенькие игры.
Автор Статьи: Тужик Александр. На момент написания статьи (2007 год) студент ДВГТУ по специальности «Вычислительные машины, комплексы, системы и сети», программированием занимаюсь не профессионально.
Введение.
Эта статья рассчитана исключительно на новичков в области игростроения, которые хотели бы научиться создавать простенькие игры своими руками. Цель статьи – помочь сделать первые шаги в этом направлении и совершить переход от обычного программирования к игровому.
Однако в тексте также объясняются некоторые общие понятия, такие как игровые конструкторы или движки, так что статья может ещё претендовать на вводный курс молодого разработчика.
Средство разработки.
Сразу оговорюсь, что наиболее простой способ создания своих игр – это модификации уже существующих или использование так называемых «игровых конструкторов». Действительно, если вам хочется поскорее создать игру приемлемого качества, не вникая во все тонкости, то это наиболее разумный выбор; точно также как человек, который хочет что-нибудь приготовить поесть, вполне может воспользоваться полуфабрикатами.
Но если вы считаете, что ваше увлечение может перерасти во что-нибудь серьёзное, то я бы советовал переходить на профессиональные языки программирования; ведь чтобы стать хорошим поваром, молодой кулинар начнёт лепить пельмени сам.
Итак, будем надеяться, что вы избрали второй путь. Теперь определимся с языком. В качестве средства разработки наших будущих игр я выбрал свой «родной» язык – Visual Basic 6.0. Но помимо моей личной симпатии к нему, есть ещё несколько причин, почему я остановил свой выбор на нём:
Во-первых, если вы уже пробовали учиться программировать, то, скорее всего, знакомы именно с этим языком или его предшественниками.
Во-вторых, это очень мощный инструмент для создания проектов любого уровня, но в то же время достаточно простой для понимания, что особенно актуально для начинающих. И даже если вы ранее не работали с Visual Basic, то изучить его основы можно без какой бы то ни было специальной подготовки; благо информации на эту тему в Интернете предостаточно.
И, наконец, в-третьих, несмотря на огромное количество желающих делать игры в Visual Basic, статей именно по созданию игр на нём (особенно на русском языке) ничтожно мало, так что я попробую хоть как-то заполнить этот пробел.
Графика.
Я позволю себе допустить, что вы уже ознакомились с языком (нам достаточно знать синтаксис базовых операторов, уметь работать с переменными, функциями и массивами, а также использовать стандартные контролы, вроде форм и кнопок). И если это так, то можно двигаться дальше.
В процессе обучения языку вы наверняка создавали небольшие приложения, но игра – это нечто большее; и настало время поговорить об одной из самых важных составляющих игр, которая отличает их от обычных программ – о графике. Неважно, что вы предпочитали при обучении азам программирования: будь то лекции в университете, книги или статьи; всё равно, до сих пор вы, скорее всего, работали только со стандартными компонентами Windows. Но я не думаю, что такая графика в игре вас устроит:
Рис 1. Игра без графики.
Разумеется, хочется добиться приемлемой двухмерной графики, как и в других играх (3D в рамках этой статьи я рассматривать не буду). Первое, что приходит на ум для этой цели – использовать PictureBox или Image в качестве картинок, при анимации менять им по кадрам свойство Picture, а при передвижении свойства Left и Top. Но при таком подходе о красивых разноцветных фонах можно забыть, так как картинки всё равно останутся прямоугольными компонентами, и нам придётся всюду «таскать» за собой небольшую рамку:
Рис 2. Космический корабль на различном фоне.
Конечно, можно пожертвовать фоном и сделать его везде монотонным, но при любом пересечении двух объектов, один из них будет оказываться впереди, а другой сзади и максимум чего мы сможем добиться, это такой картинки:
Рис 3. Пересечение объектов.
Надеюсь, этот ряд несложных примеров показал, что для создания красивой игры стандартных средств недостаточно. И здесь нам на помощь приходят графические движки, в арсенале которых имеется всё необходимое для того, чтобы избавиться от рамок вокруг изображения.
Движки.
Вот мы и добрались до движков. Читатель родился не вчера, и наверняка знает, что обычно игры делают на игровых движках, но не каждый ясно представляет себе, что же это такое. В любом случае, пугаться этого слова не стоит, движок – это всего лишь ядро программы, основа, на которой будет строиться игра. Также можно понимать движок, как некую довольно большую подпрограмму, призванную облегчить жизнь программистам.
Идея движков проста. Функционал по работе с графикой или физикой часто схож в разных играх (особенно, в одном жанре), а реализация его весьма трудоёмкий процесс. Чтобы каждый раз не писать одно и то же, имеет смысл написать все нужные функции один раз, создав движок; а потом использовать наработки для создания самих игр. Так, например, графический движок будет представлять собой набор средств по отображению графики и всевозможных визуальных эффектов. Используя такой движок, не нужно будет думать, как избавиться от рамки у картинки или сделать эффект полупрозрачности, и можно будет сосредоточиться непосредственно на игре.
Другое дело, что движки быстро устаревают, поэтому разработчикам приходится время от времени писать всё заново, создавая всё новые и новые движки в бесконечной гонке технологий.
Основное отличие игровых движков от упомянутых выше игровых конструкторов в том, что движки не связывают ваших возможностей. Используя их, вы продолжаете программировать на выбранном языке программирования; в то время как конструкторы, как правило, имеют свой синтаксис, явно уступающий обычным языкам. Продолжая тему кулинарных сравнений, затронутых в начале статьи, можно сказать, что конструкторы – это полуфабрикаты, а различные движки – это мясо и мука, без которых вам не обойтись, будучи даже профессиональным поваром.
Свои движки.
Но начиная разговор о движках, мне приходится сделать небольшое отступление о том, стоит ли писать свой движок или воспользоваться готовым. Ответ на вопрос однозначен – взять готовый. Для меня ответ очевиден, но почему-то для многих начинающих игроделов мысль о том, чтобы использовать что-то чужое, оказывается неприемлемой. Им непременно хочется сделать всё самим от начала и до конца, подражая крупным компаниям. Но это неоправданно по двум причинам:
Во-первых, потому что вы не крупная компания и на разработку движка и игры на нём может не хватить ни времени, ни сил. Сфокусироваться на игре не только намного проще, но и приятнее, ведь вы сразу будете видеть результат своих трудов, тогда как работа над движком проходит долго без какого-то бы ни было видимого результата, и может убить весь интерес к созданию игр в самом начале.
Во-вторых, вам так и так придётся использовать чужую продукцию. Не станете же вы писать свою операционную систему, или свой язык программирования. Вы воспользуетесь уже созданными, так почему же не взять готовый движок? В конце концов, и тот самый повар, готовящий пельмени, не станет делать муку сам.
Есть ещё одно мнение о том, что нужно сначала научиться создавать движки, для того, чтобы затем научиться делать игры. На самом деле наоборот, не зная, как устроены игры, нельзя создать движок. Многие ярые поклонники игр думают, что знают, что должно быть в игре, и, стало быть, знают, что должен уметь движок; но геймерского стажа здесь не достаточно. Рассуждать о том, как устроена мука не очень удобно, поэтому я возьму другой пример.
Пусть игра – это автомобиль, а движок – автомобильный цех. Допустим, вы любите машины, много ездите и знаете, что нужно водителю для полного счастья. Но вы не сможете создать свой цех, пока не будете знать, как устроена машина изнутри. Лучше зайти в чужой цех и создать в нём машину, пройдя все этапы разработки. Только после этого, вы будете иметь представление о том, что на самом деле должен уметь автомобильный цех, чтобы в нём можно было что-нибудь создать
Чужие движки.
Итак, выберем наконец-то движок для наших игр. Cамым лучшим решением, на мой взгляд, будет Software Renderer 2D (далее просто SR2D). Автор этого движка – Михаил «Mikle» Ильин, а с моего сайта можно скачать последнюю версию на момент написания статьи. Чем же этот движок хорош? Приведу несколько аргументов в его пользу:
- Он содержит всё нам необходимое, и при этом очень прост в обращении.
- Имеет множество примеров использования.
- Сделан в России, поэтому вся документация уже по-русски.
- Исходный код открыт, так что можно изучить его устройство, что будет полезно при создании своего движка.
- В случае чего, можно изменить исходный код движка под свои нужды.
- Есть версия движка для Visual Basic.net.
- И последнее, но не по важности – движок абсолютно бесплатен.
Боевая готовность.
Если вы ещё не устали от теории, то предлагаю слегка встряхнуться и приготовиться к написанию кода. Сначала проверим наличие необходимого софта. Мы должны иметь установленную копию Microsoft Visual Basic 6.0., а также распакованный архив движка SR2D.
Если всё на месте, то продолжим. Запустите Visual Basic и создайте новый проект (Standart Exe). Сразу же сохраните проект где-нибудь на жёстком диске.
Теперь нужно подключить движок. Для этого выберите пункт «Add Module» в меню «Project». В открывшемся диалоге перейдите на вкладку «Existing», найдите каталог VB6 в директории движка SR2D и подключите файл modSR2D.bas. Затем повторите процедуру, чтобы подключить модуль modUtil.bas.
Рис 4. Подключение модулей.
Для подключения классов поступайте аналогично, только вместо «Add Module», следует воспользоваться пунктом «Add Class Module». Нам потребуется только SR2D_Sprite.cls (он находиться в той же папке), а классы clsWater.cls и clsFire.cls включать в проект необязательно: они нужны для создания реалистичной воды и огня, и пока нам не понадобятся.
Осталось только скопировать SR2D.dll (всё из того же каталога) в папку с нашим проектом.
Сделаем холст.
Движок подключен, и можно приступать к созданию игры. Но если вы думаете, что мы сейчас начнём визуально расставлять компоненты, то вы ошибаетесь. В нашем расположении окажется что-то вроде холста, на котором мы будем программно формировать изображение, которое увидит игрок. Формирование изображения для нас будет сводиться к указанию что, где и как нарисовать. Причём рисовать нужно сначала дальние объекты (в том числе полупрозрачные, например, стекло), а потом ближние; как бы по слоям. Это легко понять, если представить, что мы рисуем на настоящем холсте:
Рис 5. Формирование сложного изображения слоями.
И первое, что нам нужно сделать – это как раз этот холст. В секции Generals главной формы запишем:
Option Explicit Dim Running As Boolean Dim SprFon As New SR2D_Sprite
Здесь мы включаем режим явного объявления переменных; объявляем булевскую переменную Running.
Третьей строкой мы создаём спрайт SprFon, который будет выступать в роли холста; и теперь всегда, если нам понадобиться изображения или картинки (и холст не исключение) мы будем их создавать и использовать как экземпляр класса SR2D_Sprite (или просто спрайт).
Двигаемся дальше. Добавим следующий код:
Private Sub Form_Load() Me.ScaleMode = vbPixels Me.Move Me.Left, Me.Top, 400 * Screen.TwipsPerPixelX, 300 * Screen.TwipsPerPixelY Me.Show SprFon.Init Me.ScaleWidth, Me.ScaleHeight Running = True Do While Running Render DoEvents Loop Set SprFon = Nothing Unload Me End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer) Running = False End Sub
Тут мы при загрузке формы делаем окно размером 400x300 пикселей и придаём холсту размер, равный размеру формы:
SprFon.Init Me.ScaleWidth, Me.ScaleHeight
Затем начинается выполнение цикла, где Render – это функция, формирующая изображение на холсте, и которую мы напишем чуть позже.
А функции DoEvents позволяет пользователю работать с нашим приложением (двигать курсор, нажимать кнопки, влияя на игровую ситуацию) во время выполнения цикла, а не дожидаясь его конца. Если эту строку убрать, то цикл станет выполняться бесконечно, ни на что не реагируя. Фактически это означает, что игра повиснет.
Условие, которое останавливает этот цикл выполняется только в случае выгрузки формы (Form_QueryUnload), например, при выходе из игры. Но прежде, чем закрыть приложение, мы удаляем из памяти все ранее созданные спрайты, в данном случае это только холст:
Set SprFon = Nothing
Unload Me
Теперь, как и было обещано, пишем функцию Render:
Sub Render() SprFon.PaintToHDC Me.hDC End Sub
Сейчас она содержит только одну строку – вывод изображения холста на форму, но постепенно мы будем её усложнять; и поскольку эта процедура выполняется в цикле, то от её сложности будет зависеть сколько раз обработать процедуру (а значит, и сколько раз вывести изображение на экран) будет успевать компьютер в единицу времени. Эта величина, называемая FPS (сокращение от английского frames per seconds – кадров в секунду) одна из наиболее важных характеристик производительности игры, поэтому очень полезно всегда знать её значение. Очень удобно выводить значение прямо в заголовке окна. Для этого объявим глобальную переменную FPS как Integer, в процедуру Render добавим строчку:
FPS = FPS + 1
Далее «кинем» на форму таймер, выставим ему интервал равный 1000 (одна секунда). И запишем в нём следующее:
Private Sub Timer1_Timer() Me.Caption = FPS FPS = 0 End Sub
Напоследок позаботимся о выходе из игры. Пусть это будет щелчок в любом месте формы:
Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single) Form_QueryUnload 0, 1 End Sub
Можете запустить программу и полюбоваться на чёрный холст. Значение FPS окажется довольно большое, но в процессе разработки оно будет уменьшаться. Для приятной глазу, не дёрганой картинки достаточно 24-30 кадров в секунду. Но если игра предполагает от пользователя моментальной реакции, как, например, в шутерах, то вам потребуется небольшой запас кадров, чтобы игрок не чувствовал задержки между его действиями и реакцией игры.
Рис. 6. Чистый холст.
Исходный код этого примера, как и всех последующих, вы сможете взять из этого архива.
Hello World 2.0.
Самая простая программа, которую только можно написать – это программа типа «Привет Мир!», состоящая из одной кнопки, выводящей какое-либо сообщение. Но я уже говорил, что визуально кнопки мы больше расставлять не будем, поэтому задача перестаёт быть столь тривиальной; но зато на её примере мы научимся выводить как кнопки, так и любые другие изображения.
Избавляемся от рамки.
Итак, сначала нарисуем кнопку, которую хотим видеть в игре. Чтобы пользователь знал, что это именно кнопка, а не простая картинка; следует её подсвечивать при наведении на неё курсора мыши. Поэтому сразу же сделаем два варианта кнопки:
Рис. 7. Кнопка (обычная и подсвеченная).
Обратите внимание, что весь фон, который не должен отображаться в игре, а также все прозрачные места внутри рисунка закрашиваются каким-нибудь одним цветом (красный на рисунке 7). Этот цвет можно выбрать любой, но нужно позаботиться о том, чтобы он не использовался в самом рисунке, иначе в игре он отобразится с дырками.
Стоит заметить, что SR2D различает множество цветовых оттенков, а нам нужно будет явно указать, какой является фоном; поэтому я рекомендую использовать в качестве последнего только абсолютные цвета: абсолютно чёрный, абсолютно красный, зелёный, синий или белый. Если вы пользуетесь программой MS Paint, то в его палитре эти цвета помечены крестиком на рисунке 8:
Рис. 8. Абсолютные цвета.
Если вы будете ужимать картинку, следите, чтобы сохранился цвет фона. Сжатие JPEG, например, искажает все цвета; мы же сохраним кнопки как Button1.gif и Button2.gif и положим их в папку с предыдущим проектом, который мы возьмём за основу для нашей программы (можете сохранить проект под новым именем или продолжать работать в нём).
Для начала объявим глобальную переменную n, и создадим два спрайта для наших кнопок так же, как и холст:
Dim n As Integer Dim SprKnopka(1 To 2) As New SR2D_Sprite
Не забудем их уничтожить перед выходом:
For n = 1 To 2 Set SprKnopka(n) = Nothing Next n
Спрайты созданы, но пока они пусты. Попробуем загрузить в них изображения кнопок. Для этого «бросим» на форму компонент Picture, выставим его свойства AutoRedraw и AutoSize на True, а свойство Visible установим равное False. Кроме того, нам потребуется создать дополнительную процедуру для загрузки изображения:
Public Sub SpriteLoadFromFile(Spr As SR2D_Sprite, fName As String, Optional ByVal ColorKey As Long = -1&) Picture1.Picture = LoadPicture(fName) Spr.LoadFromObject Picture1, ColorKey Picture1.Picture = Nothing End Sub
Вникать в работу этой процедуры необязательно, но благодаря ей, мы сможем загружать рисунок в спрайт через посредник (Picture1) в любом поддерживаемым Visual Basic формате (а не только tga). А выставленные свойства позволят загружать рисунок любого размера и скроют Picture от наших глаз.
Воспользуемся этой процедурой, чтобы загрузить изображения кнопок, указав в последнем параметре цвет фона:
SprFon.Init Me.ScaleWidth, Me.ScaleHeight For n = 1 To 2 SpriteLoadFromFile SprKnopka(n), "Button" & n & ".gif", ARGB(0, 255, 0, 0) Next n
Настало время поговорить о способе задания цвета. Формат описания цвета RGB – это сокращение от английского: Red, Green, Blue. В нём любой цвет представляется как смешение красного, зелёного и синего; и задаётся тремя числами от 0 до 255, обозначающих долю соответствующего цвета. Однако мы будем использовать формат ARGB, который имеет ещё четвёртое число в начале, означающее степень полупрозрачности, пока оставляйте это поле равное 0.
Так как мы использовали абсолютно красный цвет (без примесей), то значение красного будет максимальным – 255, а зелёного и синего 0.
Теперь выведем кнопку на экран. Для этого перед тем, как отобразить на экран содержимое холста, нарисуем на нём кнопку:
SprFon.Draw SprKnopka(1), 200 - SprKnopka(1).Width / 2, 150 - SprKnopka(1).Height / 2 SprFon.PaintToHDC Me.hDC
Учитывая, что наше окно 400 на 300 пикселей, то его середина будет в точке 200, 150. А поскольку функция Draw помещает в заданную точку левый верхний угол спрайта, то для того, чтобы центр кнопки совпал с центром окна мы «перемещаем» точку вывода спрайта влево ровно на половину длины спрайта и вверх на половину высоты.
Запустите программу. Как видите, мы наконец-то добились того, ради чего вообще стали использовать графический движок – красная рамка вокруг нашего спрайта исчезла:
Рис. 9. Кнопка с прозрачным фоном.
Может быть, пока кажется, что полученный результат не стоит тех стараний, которые были на него потрачены; но если продолжить, то вскоре мы добьёмся ощутимых отличий.
Курсор.
Продолжим работать над Hello World 2.0. Объявим две глобальные переменные XCur и YCur как Integer. Будем записывать в них текущее положение курсора при его перемещении:
Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) XCur = X YCur = Y End Sub
Зная положения мыши, мы можем заменить стандартный курсор своим. Сначала скрываем старый. Для этого выставляем у формы свойство Mouse Pointer на 99 (Custom), чтобы выбрать пользовательский курсор, и ставим в свойстве Mouse Icon полностью прозрачную иконку. Если вы не знаете, как такую сделать, то можете скачать уже готовый вариант.
Новый курсор – это обычная картинка, назовём её Cursor.gif и положим в папку с игрой:
Рис 10. Курсор.
Курсор выводится точно так же, как кнопка. Для тренировки проделайте эти действия сами: создайте спрайт SprCur, добавьте строку о его уничтожении, сделайте загрузку изображения через SpriteLoadFromFile, где в качестве фона укажите синий.
Не забываем, что изображение накладывается слоями, поэтому курсор надо рисовать после кнопки (так как он должен оказаться над ней), но перед выводом изображения на форму:
SprFon.Draw SprKnopka(1), 200 - SprKnopka(1).Width / 2, 150 - SprKnopka(1).Height / 2 SprFon.Draw SprCur, XCur, YCur SprFon.PaintToHDC Me.hDC
Так как место положения реального курсора и есть его левый верхний угол, то его координаты мы используем для вывода своего указателя без изменений.
Попробуйте запустить приложение. При передвижении курсора мы получаем идущий за ним шлейф из множества копий таких же курсоров:
Рис. 11. Неочищенные пиксели.
Так происходит, потому что мы не стираем старый курсор, но рисуем новый всё на том же холсте. Чтобы этого избежать, надо каждый раз очищать (закрашивать) весь холст, перед тем, как рисовать всё заново. Можно закрашивать любым цветом, например, фиолетовым:
SprFon.ClearBuffer ARGB(0, 200, 100, 255)
Теперь немного изменим функцию Render и вынесем прорисовку кнопки в отдельную процедуру DrawKnopka:
Sub Render() With SprFon .ClearBuffer ARGB(0, 200, 100, 255) DrawKnopka 200, 150 .Draw SprCur, XCur, YCur .PaintToHDC Me.hDC End With FPS = FPS + 1 End Sub
Sub DrawKnopka(X As Integer, Y As Integer) Dim Xsd As Integer, Ysd As Integer Xsd = SprKnopka(1).Width / 2 Ysd = SprKnopka(1).Height / 2 n = 1 If XCur > X - Xsd And XCur < X + Xsd And YCur > Y - Ysd And YCur < Y + Ysd Then n = 2 SprFon.Draw SprKnopka(n), X - Xsd, Y - Ysd End Sub
Мы посылаем процедуре координаты центра спрайта. Таким образом, границы спрайта отстоят от этой точки на половину его длины (высоты) в обе стороны. И если курсор лежит в этих границах, то мы отображаемой не обычную кнопку, а подсвеченную. В запущенном виде это выглядит следующим образом:
Рис. 12. Подсвеченная кнопка.
Шрифт.
Осталось вывести сообщение, а оно, как вы уже догадались, тоже создаётся картинкой. Но её можно сгенерировать, а не создавать самим. И поможет нам в этом снова посредник Picture1, на котором мы напечатает текст сообщения с помощью команды Print и передадим получившееся изображение в спрайт. Процедура будет выглядеть примерно так:
Private Sub SpriteCreateText(Spr As SR2D_Sprite, Text As String, Color As Long) Picture1.ForeColor = Color Picture1.Move 0, 0, Picture1.TextWidth(Text & " "), Picture1.TextHeight(Text & " ") Picture1.Picture = Nothing Picture1.CurrentX = 0 Picture1.CurrentY = 0 Picture1.Print Text Picture1.Picture = Picture1.Image Spr.LoadFromObject Picture1, Picture1.BackColor Picture1.Picture = Nothing End Sub
Также у Picture1 нужно будет выставить значение ScaleMode равно 3 (Pixel), BackColor установить на чёрный (&H00000000&), чтобы использовать его как фон; а тип и размер шрифта будущего спрайта настраиваются в колонке Font и выбираются исключительно по вкусу.
Давайте создадим спрайт SprText (не забываем тут же уничтожать его) и загрузим в него изображение текста:
SpriteCreateText SprText, "Hello World!", ARGB(0, 0, 150, 0)
Последние два параметра – это сам текст сообщения и цвет надписи.
Чтобы надпись отображалась после нажатия кнопки, переделаем обработку Form_MouseDown. Вместо выхода из программы запишем:
Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single) If n = 2 Then Vkl = True End Sub
Где Vkl – это новая глобальная переменная типа Boolean, а значение n во время выполнения цикла Render, если вы помните из предыдущих примеров, у нас будет равно двум,
только если курсор находится над кнопкой.
Для красоты можно чуть-чуть «поднять» кнопку вверх, а надпись выводить под ней:
DrawKnopka 200, 100 If Vkl = True Then .Draw SprText, 200 - SprText.Width / 2, 200 - SprText.Height / 2
Работу над созданием Hello World 2.0. мы закончили. Как должен выглядеть окончательный результат вы можете посмотреть на рисунке 13, а исходный код этой программы и всех промежуточных стадий вы найдёте в том же архиве=http://www.alprog.ru/Article/Source.rar с примерами, о котором я уже говорил раньше.
Рис. 13. Hello World.
Тетрис.
Конечно, Hello World – это самая маленькая программа вообще, но так уж сложилась, что в игровой индустрии наименьшей единицей измерения является тетрис. И если мы хотим приобщиться к созданию игр, то наиболее разумным будет начинать именно с него. Итак, нашей первой игрой будет тетрис.
Полноэкранный режим.
До сих пор наша программа отображалась в окне, и нас это устраивало. Но тетрис – это уже полноценная игра, а игры смотрятся намного привлекательнее в полноэкранном режиме. И если мы хотим развернуть приложение на весь экран, то нам придётся считаться с тем, что у разных пользователей стоит разное расширение. Таким образом, нам следует научиться менять расширение экрана.
Продолжим старый проект и создадим для этих целей отдельный модуль. Перейдите в меню «Project» >> «Add Module» >> «New» >> «Module». Появившийся модуль переименуйте в Resolution и откройте его двойным щелчком:
Рис 14. Модуль Screen.
Теперь вставим в модуль следующий код:
Private Declare Function ChangeDisplaySettings Lib "user32" Alias "ChangeDisplaySettingsA"_ (lpDevMode As Any, ByVal dwflags As Long) As Long Private Declare Function EnumDisplaySettings Lib "user32" Alias "EnumDisplaySettingsA"_ (ByVal lpszDeviceName As Long, ByVal iModeNum As Long, lpDevMode As Any) As Boolean Const DM_PELSWIDTH = &H80000 Const DM_PELSHEIGHT = &H100000 Const CCFORMNAME = 32 Const CCDEVICENAME = 32 Private Type DEVMODE dmDeviceName As String * CCDEVICENAME dmSpecVersion As Integer dmDriverVersion As Integer dmSize As Integer dmDriverExtra As Integer dmFields As Long dmOrientation As Integer dmPaperSize As Integer dmPaperLength As Integer dmPaperWidth As Integer dmScale As Integer dmCopies As Integer dmDefaultSource As Integer dmPrintQuality As Integer dmColor As Integer dmDuplex As Integer dmYResolution As Integer dmTTOption As Integer dmCollate As Integer dmFormName As String * CCFORMNAME dmUnusedPadding As Integer dmBitsPerPel As Integer dmPelsWidth As Long dmPelsHeight As Long dmDisplayFlags As Long dmDisplayFrequency As Long End Type Public Sub ChangeResolution(iWidth As Single, iHeight As Single) Dim DevM As DEVMODE Dim a As Boolean Dim i As Long Dim b As Long i = 0 Do a = EnumDisplaySettings(0&, i&, DevM) i = i + 1 Loop Until (a = False) DevM.dmFields = DM_PELSWIDTH Or DM_PELSHEIGHT DevM.dmPelsWidth = iWidth DevM.dmPelsHeight = iHeight b = ChangeDisplaySettings(DevM, 0) End Sub
Здесь нам интересна только процедура ChangeResolution. Посылая ей значение ширины и высоты, мы сможем в любой момент сменить расширение экрана.
Вернёмся на основную форму. При запуске программы надо «запомнить» старое расширение экрана, поменять его на нужное в игре и сделать размер окна равным размеру экрана:
OldX = Screen.Width / Screen.TwipsPerPixelX OldY = Screen.Height / Screen.TwipsPerPixelY ChangeResolution 800, 600 Me.ScaleMode = vbPixels Me.Move 0, 0, 800 * Screen.TwipsPerPixelX, 600 * Screen.TwipsPerPixelY Me.Show
OldX и OldY – это глобальные переменные типа Single, которые нужны для того, чтобы перед выходом из игры можно было восстановить прежнее расширение экрана:
ChangeResolution OldX, OldY
Осталось только убрать строку заголовка. Сделаем у формы свойство ControlBox = False, а свойство Caption сделаем пустым.
Соответственно и FPS больше нельзя выводить в заголовке окна. Будем сохранять его в новый спрайт как текст:
Private Sub Timer1_Timer() SpriteCreateText SprFPS, Str(FPS), ARGB(0, 255, 255, 255) FPS = 0 End Sub
Выведем на экран значение FPS перед курсором:
.Draw SprFPS, 0, 0
Осталось только позаботиться о выходе из игры (так как кнопки «закрыть» больше нет). Пусть выход будет при нажатии на кнопку, место вывода Hello World:
If n = 2 Then Form_QueryUnload 0, 1
Теперь можете убрать всё, что связано со спрайтом SprText и переменной Vkl (остатки от проекта Hello World, которые нам больше не пригодятся) и запустить программу. Если всё сделано верно, то вы получите нечто вроде этого:
Рис. 15. Полноэкранный режим.
Игровое поле.
Окно развёрнуто на весь экран, но игровое поле тетриса занимает далеко не всё пространство. Падающие кубики расположатся в правой части экрана, слева можно сделать меню, но всё оставшееся место будет пустовать. Поэтому, если вам ещё не надоел монотонный фиолетовый, то давайте сменим фон. Загрузим обычную картинку 800x600 как спрайт SprBack и будем выводить её сразу после очистки холста.
Ещё два необходимых спрайта – это два вида кубика: падающий, и уже упавший:
Рис 16. Спрайты кубика.
Загрузим их (SprCube) по аналогии с тем, как мы ранее загружали два вида кнопки:
Dim SprBack As New SR2D_Sprite Dim SprCube(0 To 2) As New SR2D_Sprite For n = 1 To 2 SpriteLoadFromFile SprKnopka(n), "Button" & n & ".gif", ARGB(0, 255, 0, 0) SpriteLoadFromFile SprCube(n), "Cube" & n & ".jpg" Next n SpriteLoadFromFile SprBack, "Back.jpg"
Обратите внимание на то, что мы создали три спрайта кубика, а загрузили только два. Нулевой спрайт специально создаём и оставляем пустым: при попытке вывести его на холст, спрайт выродится в точку, что позволит не писать обработчик для пустой клетки, а заодно обеспечит разметку поля в виде чёрных точек.
Все спрайты загружены, так что можно приступать к созданию самого игрового поля. Оно представляет собой матрицу размерами 10x25, где каждая ячейка может принимать одно из трёх значений: 0, 1 или 2, означающих номер нужного спрайта кубика. Также нам потребуется дополнительная матрица для хранения и операций с фигурами. В классическом тетрисе любая фигура помещается в квадрат 4x4; поэтому, учитывая, что нумерация начинается с нуля, запишем:
Dim Pole(9, 24) As Byte Dim Matrix(3, 3) As Boolean Dim X As Integer, Y As Integer Dim m As Integer, k As Integer
Поскольку дополнительная матрица будет соответствовать какой-то области на поле, и эта область будет меняться, то мы ввели две переменные X и Y, в которых мы будем хранить координаты ячейки на игровом поле, соответствующей верхней левой ячейке дополнительной матрицы.
Ну а m и k это просто служебные переменные, которые понадобятся нам в цикле вывода игрового поля:
For m = 0 To 9 For k = 0 To 24 .Draw SprCube(Pole(m, k)), 500 + m * 23, 12 + k * 23 Next Next
Запустите программу. Мы должны увидеть фоновый рисунок и множество точек на месте клеток:
Рис. 17. Разметка точками.
Перемещение фигур.
Сделаем появление фигуры при старте программы:
Pole(4, 0) = 1 Pole(4, 1) = 1 Pole(5, 1) = 1 Pole(5, 2) = 1 X = 3 Y = 0
Так как X и Y означают левый верхний угол дополнительной матрицы, то мы задаём её область по x с 3 по 6 ячейку, и по y с 0 по 3. В эту область попадает наша фигура, значит, мы сможем совершать над ней преобразования. Чтобы видеть, где сейчас находится матрица, с которой мы работает, создадим и загрузим прозрачный спрайт SprRama:
Рис. 18. Рама.
Чтобы спрайт отображался в игре, в цикл рисования точек добавим строчку:
If m = X And k = Y Then .Draw SprRama, 500 + m * 23, 12 + k * 23
Рис 19. Рабочая матрица.
Первое преобразование рабочей матрицы, которое мы сделаем – это перемещение. Мы можем двигать фигуры влево, вправо и вниз. Напишем обработку нажатий соответствующих клавиш:
Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer) If KeyCode = vbKeyLeft Then Dvig X - 1, Y If KeyCode = vbKeyRight Then Dvig X + 1, Y If KeyCode = vbKeyDown Then Dvig X, Y + 1 End Sub
При нажатии любой из этих кнопок запускается процедура Dvig, параметрами которой будут координаты матрицы после соответствующего перемещения. Другими словами, указывается куда должно «стать» матрица. Несмотря на кажущуюся простоту, процедура Dvig достаточно сложна. Мы не можем просто прогнать цикл, в котором поэлементно копируются клетки из старой области в новую, потому что при движении этим методом в одну сторону, мы оставляем за собой неубранные кубики, а при движении в другую и вовсе уничтожаем всю фигуру, как показано на рисунке:
Рис 20. Перемещение матрицы.
И вот как раз здесь нам и пригодится матрица 4x4, которую мы создали в предыдущем примере. Алгоритм перемещения фигуры будет примерно такой:
1. Копируем рабочую область в дополнительную матрицу;
2. Проверяем можно ли поставить эту матрицу на новое место и если да то:
3. Удаляем фигуру из рабочей области;
4. Ставим на новое место фигуру из дополнительной матрицы;
5. Меняем координаты рабочей области на новые.
Первый и третий пункт – это однотипные циклы; поэтому мы реализуем их как процедуру, вызываемую из основного алгоритма:
Sub Dvig(NewX As Integer, NewY As Integer) Deistv "Copy" If Proverka(NewX, NewY) Then Deistv "Delete" For m = 0 To 3 For k = 0 To 3 If Matrix(m, k) = 1 Then Pole(NewX + m, NewY + k) = Matrix(m, k) Next Next X = NewX Y = NewY End If End Sub
Sub Deistv(H As String) For m = 0 To 3 For k = 0 To 3 If H = "Copy" Then Matrix(m, k) = Pole(X + m, Y + k) If H = "Delete" And Pole(X + m, Y + k) = 1 Then Pole(X + m, Y + k) = 0 Next Next End Sub
Заметьте, что при удалении фигуры, мы уничтожаем только ячейки с падающим кубиком (Pole(X + m, Y + k) = 1); а кубики уже упавшие, остаются на своих местах.
Ещё, прежде, чем мы продолжим, надо подумать об ограничении перемещений у границы. Ведь иногда, когда рамка находится вплотную к стенке, а сама фигура – нет, то перемещение сделать можно, но при этом рамка должна остаться на месте:
Рис 21. Перемещение у границы.
При этом движении, пустые кубики слева пытаются «выскочить» за пределы игрового поля, что может вызвать ошибку; поэтому внесём в Dvig некоторые изменения:
For m = 0 To 3 For k = 0 To 3 If NewX + m >= 0 And NewX + m < 10 And NewY + k < 25 Then If Matrix(m, k) = 1 Then Pole(NewX + m, NewY + k) = Matrix(m, k) End If Next Next If NewX >= 0 And NewX < 7 Then X = NewX If NewY < 22 Then Y = NewY
Здесь мы, перед тем как переместить каждый кубик, проверяем, не выходит ли он за пределы поля. И в конце точно также проверяем положение рамки; учитывая, что слева и внизу от её координат должны поместится по три кубика (10-3=7, 25-3=22).
Что ж, движение во всех разрешённых случаях у нас реализовано. Осталось написать только функцию Proverka, чтобы предотвратить перемещение на те клетки, куда этого сделать нельзя. В первой части процедуры проверяем, не попадёт ли какой-нибудь из наших кубиков на клетку, уже занятую упавшим кубиком:
Function Proverka(NewX As Integer, NewY As Integer) As Boolean Proverka = True For m = 0 To 3 For k = 0 To 3 If NewX + m > -1 And NewX + m < 10 And NewY + k < 25 Then If Pole(NewX + m, NewY + k) = 2 And Matrix(m, k) = 1 Then Proverka = False End If Next Next …
А заканчиваем тремя блоками на случай, если рамка двигается за какой-нибудь край поля, и при этом, на её прислонённой грани есть не пустые клетки:
… If NewX < 0 Then m = 0 For k = 0 To 3 If Matrix(m, k) = 1 Then Proverka = False Next End If If NewX >= 7 Then m = 3 For k = 0 To 3 If Matrix(m, k) = 1 Then Proverka = False Next End If If NewY >= 22 Then k = 3 For m = 0 To 3 If Matrix(m, k) = 1 Then Proverka = False Next End If End Function
Наконец-то движение сделано, так что можете откомпилировать код и убедиться в том, что оно происходит корректно во все стороны.
Падение и исчезновение.
Следующих шаг на пути создания тетриса – это падение фигур. Поставим на форму второй таймер. Пусть он каждую секунду вызывает процедуру
Dvig X, Y + 1
Кубики начнут опускаться не только при нажатии кнопки вниз, но и по времени. Но это ещё не полноценное падение, ведь если фигура «наткнётся» на препятствие (пол или занятые ячейки), то нужно все её кубики сделать «застывшими» и создать вверху экрана новый предмет. Сделаем для этих целей по процедуре.
С застыванием всё просто, это такой же поэлементный цикл, как удаление или копирование, поэтому просто добавим строку в процедуру Deistv:
If H = "Zastiv" And Pole(X + m, Y + k) = 1 Then Pole(X + m, Y + k) = 2
Теперь перечислим координаты кубиков, которыми можно задать 7 типов тетрисных фигур и сделаем процедуру появления случайного предмета:
Sub NewElement() Dim a As Byte a = Int(Rnd * 7) Select Case a Case 0 'Палка Pole(4, 0) = 1 Pole(4, 1) = 1 Pole(4, 2) = 1 Pole(4, 3) = 1 Case 1 'Пол креста Pole(4, 0) = 1 Pole(3, 1) = 1 Pole(4, 1) = 1 Pole(4, 2) = 1 Case 2 'Квадрат Pole(4, 1) = 1 Pole(4, 2) = 1 Pole(5, 1) = 1 Pole(5, 2) = 1 Case 3 'Ход конём Pole(4, 0) = 1 Pole(5, 0) = 1 Pole(4, 1) = 1 Pole(4, 2) = 1 Case 4 'Зеркальный ход конём Pole(5, 0) = 1 Pole(4, 0) = 1 Pole(5, 1) = 1 Pole(5, 2) = 1 Case 5 'Молния Pole(4, 0) = 1 Pole(4, 1) = 1 Pole(5, 1) = 1 Pole(5, 2) = 1 Case 6 'Зеркальная молния Pole(5, 0) = 1 Pole(5, 1) = 1 Pole(4, 1) = 1 Pole(4, 2) = 1 End Select X = 3 Y = 0 End Sub
Не забудьте в Form_Load убрать появление первой фигуры, ведь она тоже должна выбираться случайным образом:
Randomize
NewElement
Итак, процедуры готовы. Осталось их только вызвать при падении и контакте с препятствием. Cделаем это, когда при попытке движения (dvig), перемещение было запрещено (Proverka = false), а это движение совершалось вниз (NewY > Y):
Sub Dvig(NewX As Integer, NewY As Integer) Deistv "Copy" If Proverka(NewX, NewY) Then … … Else If NewY > Y Then Deistv "Zastiv" Lines NewElement End If End If End Sub
Третья функция Lines – это проверка, не собрана ли какая-либо из линий целиком, которую очень удобно делать между этими функциями; поймав момент, когда кубики уже застыли, но новые не появились, а, значит, рабочая матрица всё ещё внизу. Её координата Y нам нужна для того, чтобы не перебирать все строки, а проверить только те, на которые упал кубик:
Sub Lines() Dim Polnoe As Boolean For k = Y To Y + 3 Polnoe = True For m = 0 To 9 If Pole(m, k) = 0 Then Polnoe = False Next If Polnoe = True Then Sdvig k Next End Sub
Здесь, если мы находим собранную линию, то вызываем процедуру Sdvig, чтобы все линии, которые находятся выше её, опустились на одну ячейку вниз:
Sub Sdvig(ByVal Start As Byte) Dim L As Integer, p As Integer For L = Start To 5 Step -1 For p = 0 To 9 Pole(p, L) = Pole(p, L - 1) Next Next End Sub
Причём мы спускаем строки поочерёдно, идя от нижней к верхней, и не затрагивая при этом последние пять, так как там должно быть пустое пространство (в противном случае игрок проиграл).
Что ж, фигурки появляются, падают и исчезают. То, что у нас получилось, уже начинает напоминать тетрис:
Рис 22. Застывающие кубики.
Вращение фигур.
Несмотря на то, что почти всё сделано, толком поиграть ещё нельзя. А всё потому, что фигуры пока нельзя переворачивать. И хотя это, наверное, самая сложная часть тетриса, нам её будет сделать довольно легко, потому что мы с самого начала делали всё с использованием дополнительной матрицы, и поворот фигуры будет сводиться к повороту этой матрицы.
Итак, поворот матрицы вправо – это процесс, когда номер столбца равен номеру строки соответствующей ячейки в новой матрице, а номер строки равен номеру столбца ячейки в новой матрице, если считать с конца. При повороте влево, естественно, тоже, но наоборот. Это легко понять, если взглянуть на рисунок:
Рис. 23. Поворот матрицы.
Таким образом, поворот практически не отличается от обычного копирования из рабочей области. Добавим две новых строки в Deistv:
If h = "Copy" Then Matrix(m, k) = Pole(X + m, Y + k) If h = "Vpravo" Then Matrix(3 - k, m) = Pole(X + m, Y + k) If h = "Vlevo" Then Matrix(k, 3 - m) = Pole(X + m, Y + k)
Пусть эти повороты будут происходить при нажатии клавиш Q и E:
If KeyCode = vbKeyQ Then Dvig X, Y, "Vlevo" If KeyCode = vbKeyE Then Dvig X, Y, "Vpravo"
Здесь мы вызываем всё ту же процедуру Dvig, только посылаем старые координаты без изменений (так что рабочая матрица не двигается), и указываем ещё один необязательный параметр, обозначающий действие, которое надо сделать с матрицей место копирования (поворот влево или вправо).
Осталось только добавить этот параметр в саму процедуру и поменять в ней первую строку:
Sub Dvig(NewX As Integer, NewY As Integer, Optional Param As String = "Copy") Deistv Param …
Если Param не будет задан явно, то по умолчанию выполнится копирование, так что все старые вызовы процедуры останутся корректными.
Вот и всё, наш тетрис практически готов. Но не время расслабляться, надо ещё «навести красоту», приделав нормальное меню, очки, и тому подобные вещи.
Наводим красоту.
Для начала давайте сделаем две кнопки место одной: «Новая игра» и «Выход». Просто вывести два раза одну и ту же кнопку нам недостаточно, ведь нам нужно их отличать. Поэтому добавим в процедуру вывода кнопки третий параметр, его уникальный индекс:
Sub DrawKnopka(X As Integer, Y As Integer, Index As Single)
И дальше, в теле процедуры, мы будем использовать его, чтобы присвоить глобальной переменной KnopkaNum индекс подсвеченной кнопки:
If XCur > X - Xsd And XCur < X + Xsd And YCur > Y - Ysd And YCur < Y + Ysd Then n = 2 KnopkaNum = Index End If
Разумеется, действия по нажатию этих кнопок тоже будут различны:
If KnopkaNum = 1 Then For m = 0 To 9 For k = 0 To 24 Pole(m, k) = 0 Next Next NewElement Game = True End If If KnopkaNum = 2 Then Form_QueryUnload 0, 1
Здесь вторая кнопка – это выход из игры, а первая должна начать игру заново. Для этого мы очищаем поле ото всех кубиков, создаём новый элемент (падающую фигуру), и включаем переменную Game. Она должна быть объявлена глобально, и в случае, если она равна False, игра должна останавливаться (для этого просто допишите проверку значения Game в процедуре Dvig).
Остановка игры происходит, когда застывшие кубики добрались до 5 строчки, поэтому проверку сделаем при их застывании:
Deistv "Zastiv" Lines For m = 0 To 9 If Pole(m, 4) = 2 Then Game = False Next If Game = True Then NewElement
Заметьте, что если игра остановилась, то выводить новый элемент не нужно (он появится, когда мы нажмём «Новая игра»).
Последний штрих – это очки и возрастание скорости. Сделаем их вывод текстом, а заодно и подпишем кнопки. На всё про всё нам понадобится 6 текстовых спрайтов и 2 переменные для хранения количества очков и текущего уровня:
Dim KnopkaNum As Byte Dim Game As Boolean Dim Ochki As Integer, Level As Integer Dim SprText(1 To 6) As New SR2D_Sprite
Загрузим спрайты в Form_Load:
SpriteCreateText SprText(1), "Новая игра", ARGB(0, 200, 200, 0) SpriteCreateText SprText(2), "Выход", ARGB(0, 200, 200, 0) SpriteCreateText SprText(3), "Уровень:", ARGB(0, 0, 255, 255) SpriteCreateText SprText(4), "Игра:", ARGB(0, 0, 255, 255) Obnov
Последние два спрайта, выводящие значения переменных, будут меняться, поэтому они вынесены в процедуру Obnov, которая выглядит так:
Sub Obnov() SpriteCreateText SprText(5), Str(Level), ARGB(0, 0, 255, 255) SpriteCreateText SprText(6), Str(Ochki), ARGB(0, 0, 255, 255) Level = Int(1 + Sqr(Ochki / 500)) Timer2.Interval = 1000 / Sqr(Level) End Sub
Здесь мы ещё устанавливаем уровень сложности в зависимости от набранных очков, и меняем скорость падения кубиков. В моём примере получается следующая таблица:
Очки можно начислять при падении кубика (количество очков зависит от уровня):
Ochki = Ochki + 10 * Level Obnov
А также при исчезновении линий:
Ochki = Ochki + 100 * Level
Осталось только вывести на экран все надписи и кнопки (не забываем, что у кнопок есть уникальный индекс):
TextDraw 100, 50, 1 DrawKnopka 100, 100, 1 TextDraw 100, 150, 2 DrawKnopka 100, 200, 2 TextDraw 250, 50, 3 TextDraw 250, 150, 4 TextDraw 250, 100, 5 TextDraw 250, 200, 6
Процедура TextDraw примет такой вид:
Sub TextDraw(X As Integer, Y As Integer, Num As Byte) SprFon.Draw SprText(Num), X - SprText(Num).Width / 2, Y - SprText(Num).Height / 2, OpAdd End Sub
Теперь уберём вывод FPS и рамки рабочей матрицы; добавим обнуление счёта при старте новой игры; и тетрис готов. Можете себя поздравить, мы только что сделали нашу первую игру. Если теперь откомпилировать exe-файл и запустить, то мы должны увидеть примерно такую картинку:
Рис 24. Финальный вариант тетриса.
Кстати, если вам во время игры в оригинальный тетрис казалось, что разработчики специально сделали так, что на высокой сложности нужная фигра выпадает реже, нежели всё остальное; то теперь вы можете убедиться, что заветная «палка» не попадает к нам в руки исключительно по закону подлости :)
В заключение напомню, что если вы до сих пор не скачали исходный код нашего тетриса, то можете скачать его сейчас.
Что делать дальше?
Теперь вы можете посмотреть в папке движка примеры реализации различных эффектов, таких как огонь, вода, бамп мэппинг:
Рис 25. Свет и огонь.
После этого вы можете приступать к созданию своей игры. Но не стоит замахиваться на что-то большое. Не стоит браться за игры в жанре RPG или RTS. Я бы советовал начать с арканоида или в крайнем случае, аркадного скролера или платформера.
На этом я закончу свою статью, если у вас возникли какие-то вопросы, связаться со мной можно так:
e-mail:
ICQ: 254110101
Я понимаю, что читать это всё лениво; но, может быть, хоть как-нибудь откомментируете?
Alprog
Жесть. Респект за проделанный труд. К сожалению, а может и к счастью, мой путь развия шел не через спрайтово-тайловые игры и вижуал бейсик, как у тебя, поэтому прокомментировать затруднюсь и просто еще раз выражу респект :)
//омг, злобный орк с темным эльфом особенно порадовали :D
Suslik
Спасибо :)
Кстати, тетрис я впервые в жизни писал как раз-таки для статьи.
Alprog
>Кстати, тетрис я впервые в жизни писал как раз-таки для статьи.
Маленькое и Очень скромное РПГ ты тоже для статьи пишешь? =)
Меня почему-то, когда я только начинал заниматься программированием, тянуло к максимальной свободе действий - то есть никаких тайлов, никакой сетки и ключей прозрачности. Моя первая змейка могла поворачиваться на любой угол, не обязательно на 90o - отлично помню, как синусы и косинусы мне в шестом классе объяснял дед, который сам-то с трудом все помнил :D Но у такого подхода есть свои очевидные минусы - я бОльшую часть времени не игры писал, а пытался разбираться в каких-то учебниках, узнавал у старших основы математики, было забавно наблюдать, как мама пыталась сгенерить мне какую-нибудь информацию по афинным преобразованиям - мне очень хотелось нарисовать спрайт, заданный своими четырьмя углами :D
Suslik
>Маленькое и [b]Очень[/b] скромное РПГ ты тоже для статьи пишешь? =)
На что намекаешь? 8)
Alprog
Только на твою скромность! Ни на что более! :)
Статью полностью не читал. Думаю если ты хочешь чтобы она была полезна нубам, там надо ставить коменты после каждой строки.
А вот таких записей ...
Private Declare Function EnumDisplaySettings Lib "user32" Alias "EnumDisplaySettingsA"_
(ByVal lpszDeviceName As Long, ByVal iModeNum As Long, lpDevMode As Any) As Boolean
...я бы испугался и убежал:)
Тема в архиве.