// - "Talk to me like I'm a 3 year old!" Programming Lessons -
//
// $Author: Ben Humphrey digiben@gametutorials.com
//
// $Program: MoveMan
//
// $Description: Using the arrow keys and mouse to move a character around
//
// $Date: 6/20/00
//
// Перевод © 2004 Евгений Казеко
// www.gamecoder.kazeko.com
// evgeniy@kazeko.com
// Мы подключаем наш заголовочный файл, в котором содержатся
// все остальные заголовки, функции и структуры
#include "MoveMan.h"
// Создаем переменные HANDLE для ввода и вывода. Мы не помещаем их в main(), потому что
// мы хотим, чтобы эти переменные были глобальные. Это значит, все функции, а не только main, смогут
// использовать их.
HANDLE hInput, hOutput;
// Определяем действия, выполняемые функцией.
void DrawPlayer(PLAYER Player, int Draw)
{
if(Draw) // Если "Draw" == DRAW (в заголовке определено #define DRAW 1)
{
// Устанавливаем положение курсора соответствующее положению игрока
SetConsoleCursorPosition(hOutput, Player.Position);
// Печатаем на экране символ '@' туда, где находится курсор
printf("@");
}
else // Если "Draw" == ERASE, значит мы хотим удалить символ
{
// Мы хотим затереть символ пробелом ' ', поэтому мы устанавливаем курсор на положение игрока.
SetConsoleCursorPosition(hOutput, Player.Position);
printf(" ");
// И теперь, когда мы печатаем пробел, он затрет последний символ '@'.
// Если мы не будем стирать символ игрока, получится, что как будто бы он оставляет за собой след.
}
}
/* Здесь мы создаем функцию MovePlayer().
INPUT_RECORD это наша структура, в которой содержится информация о буфере ввода
(о том, что делает пользователь).
Мы также создаем переменную типа DWORD для совместимости с ReadConsoleInput().
(В переменной будет храниться количество произошедших событий).
Обратите внимание, что мы помещаем эти переменные в данную функцию,
потому что они не будут использоваться другими функциями.
*/
void MovePlayer(PLAYER *Player)
{
INPUT_RECORD InputRecord;
DWORD Events=0;
// Читаем действия пользователя и сохраняем информацию в структуру InputRecord.
ReadConsoleInput(hInput, &InputRecord, 1, &Events);
/* Ниже мы используем переменную "EventType". По этой переменной можно определить, было ли событие
связано с мышью или с клавиатурой. (Использовали ли мы мышь или клавиатуру).
MOUSE_EVENT определено где-то в Windows. Мы проверяем, была ли использована мышь.
*/
if(InputRecord.EventType == MOUSE_EVENT)
{
/* Если мы использовали мышь, проверяем, была ли нажата левая клавиша мыши. Значение
FROM_LEFT_1ST_BUTTON_PRESSED также определено где-то в Windows.
В предыдущей обучалке мы использовали "...Event.KeyEvent....". В этот раз мы проверяем
информацию о мыши. Внутри структуры "MouseEvent" есть переменные, которые содержат данные
о нажатых клавишах мыши, положении мыши, а также была ли при этом нажата клавиша CONTROL.
Есть и другие переменные.
Проверим, была ли нажата левая клавиша мыши.
*/
if(InputRecord.Event.MouseEvent.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED)
{
DrawPlayer(*Player, ERASE);
/* Здесь мы вызываем функцию DrawPlayer(), передавая в нее ERASE. Перед тем как нарисовать
новый символ, мы хотим удалить старый.
Обратите внимание на "*" перед "Player". Поскольку мы уже передали в данную функцию
адрес структуры "Player" в памяти, "Player" является указателем.
Посмотрите на определение функции DrawPlayer(): DrawPlayer(PLAYER Player, int Draw);
Эта функция не запрашивает адрес "Player" (PLAYER Player). Ей нужны данные из структуры,
а не ее адрес. Поэтому мы и помещаем впереди указателя символ "*", давая компилятору
понять, что мы передаем саму переменную.
Если мы нажали левую клавишу мыши, поместим символ игрока туда, где был щелчок мышью.
Видите стрелку, указывающую на "Position.X"? Когда у вас указатель на структуру, которой
является "Player", и мы хотим получить доступ к ее переменным, мы должны использовать
стрелку вместо точки. Это только для указателей. Обратите внимание, что стрелка не
нужна для "Position". "Position" это не указатель, "Player" - указатель. Это может
показаться сложным, но когда вы больше будете использовать указатели, все станет
понятнее.
Устанавливаем игрока туда, где была нажата мышь. Положение места, где она была нажата,
хранится в InputRecord.Event.MouseEvent.dwMousePosition.
dwMousePosition имеет тип COORD. Поэтому для доступа к значению Х, используем ".X"
после dwMousePosition. То же и для Y.
*/
Player->Position.X = InputRecord.Event.MouseEvent.dwMousePosition.X;
Player->Position.Y = InputRecord.Event.MouseEvent.dwMousePosition.Y;
// Здесь мы рисуем символ на новом положении,
поэтому мы вновь вызываем DrawPlayer() с параметром DRAW.
DrawPlayer(*Player, DRAW);
}
}
/* Ниже мы проверяем, была ли нажата клавиша, и берем только нажатие "вниз".
Чтобы убедиться, что клавиша была только нажата (вниз) а не отжата (вверх), мы проверяем bKeyDown.
Это не нужно для Windows 95 и 98, но системы, основанные на NT обрабатывают консольные
функции по-другому. В Windows 95 и 98 отслеживается только нажатие клавиши, в то время как
системы NT отслеживают и нажатие и отжатие клавиши. В результате игрок будет двигаться
каждый раз на 2 позиции, поэтому нам нужно удостовериться, что мы перемещаем игрока только
при нажатии клавиши (вниз). Мы используем переменную bKeyDown, и проверяем ее на истинность.
*/
if(InputRecord.EventType == KEY_EVENT && InputRecord.Event.KeyEvent.bKeyDown)
{
// Проверяем, была ли нажата стрелка вправо.
if(InputRecord.Event.KeyEvent.wVirtualKeyCode == VK_RIGHT)
{
DrawPlayer(*Player, ERASE); // Удаляем символ на текущей позиции, передавая в функцию ERASE.
Player->Position.X++; // Увеличиваем значение X, потому что мы двигаемся вправо.
DrawPlayer(*Player, DRAW); // Рисуем символ игрока на новой позиции, передавая DRAW.
}
// Проверяем, была ли нажата стрелка влево.
else if(InputRecord.Event.KeyEvent.wVirtualKeyCode == VK_LEFT)
{
DrawPlayer(*Player, ERASE);
Player->Position.X--; // Уменьшаем значение X, потому что мы двигаемся влево.
DrawPlayer(*Player, DRAW);
}
// Проверяем, была ли нажата стрелка вверх.
else if(InputRecord.Event.KeyEvent.wVirtualKeyCode == VK_UP)
{
DrawPlayer(*Player, ERASE);
Player->Position.Y--; // Уменьшаем значение Y, так как мы двигаемся вверх.
DrawPlayer(*Player, DRAW);
}
// Проверяем, была ли нажата стрелка вниз.
else if(InputRecord.Event.KeyEvent.wVirtualKeyCode == VK_DOWN)
{
DrawPlayer(*Player, ERASE);
Player->Position.Y++; // Увеличиваем значение Y, так как мы двигаемся вниз.
DrawPlayer(*Player, DRAW);
}
}
FlushConsoleInputBuffer(hInput); // Очищаем буфер ввода.
}
void main()
{
// Объявляем переменную типа PLAYER. Взгляните на MoveMan.h,
чтобы увидеть созданную нами структуру.
PLAYER Player;
// Инициализируем переменные для буфера ввода и вывода.
hInput = GetStdHandle(STD_INPUT_HANDLE);
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleMode(hInput, ENABLE_PROCESSED_INPUT | ENABLE_MOUSE_INPUT);
/* Устанавливаем консольный режим, чтобы был возможет ввод мышью и выход по CONTROL-C.
Теперь мы сможем использовать мышь. Видите оператор "|" OR? Этот оператор
производит побитовое объединение двух чисел. Это бинарная операция, которая устанавливает
нулевые биты в 1.
Небольшой урок по двоичной системе. (Не ждите, что вы сразу все поймете).
Число 8 в двоичной системе записывается так: "1000".
Число 2 в двоичной системе записывается так: "0010".
Откуда я взял эти числа? Эти числа читаются справа налево.
0 = "0000" 1 = "0001" 2 = "0010" 4 = "0100" and 8 = "1000"
Если мы сложим 2 "0010" + 1 "0001" = "0011" -- мы получим 3.
Вот что делает оператор OR.
"8: "1000"
OR "2: "0010"
получаем "10: "1010"
Итак, 8 OR 2 становится 10, потому что мы объединяем биты, если на одинаковых позициях стоят 0 и 1.
8: "1000"
OR 8: "1000"
= 8: "1000:
8 OR 8 так и остается 8, потому что у них у обоих стоит 1 в первой позиции.
В основном этот оператор сравнивает 2 числа и устанавливает нулевые биты на одинаковых позициях в 1,
если они равны нулю.
Не расстраивайтесь, если сразу не все понятно. Посмотрите обучалку "Бинарные операторы".
Пока что запомните, что в данном случае этот оператор позволяет вводить 2 опции.
То что делает функция - берет маску, по которой проверяется, какие биты установлены в 1.
*/
Player.Position.X = SCREEN_WIDTH / 2; // Мы хотим начать в центре экрана.
Player.Position.Y = SCREEN_HEIGHT / 2; // Поэтому мы делим ширину и высоту экрана на 2.
// Вызываем функцию для установки положения курсора согласно данным струкруры Player.
SetConsoleCursorPosition(hOutput, Player.Position);
////////////////////////////// Игровой цикл ////////////////////////////////
/* Создадим бесконечный цикл, для того, чтобы проверять ввод с клавиатуры или мыши.
Условие while(1) всегда истинно, поэтому цикл не остановится, пока мы не закроем окно или
не нажмем CONTROL-C.
*/
while(1)
{
MovePlayer(&Player); // Вызываем нашу функцию движения игрока.
}
/* Не правда ли, наш игровой цикл хорошо смотрится? Всего один вызов функции.
Именно так и должен выглядеть игровой цикл. Только вызовы функций. Может быть еще пара
условных операторов. Идея заключается в том, что этот цикл должен быть простым. Мы
создаем функции, контролирующие ход игры, и затем вызываем их в игровом цикле.
Позже вы увидите "while(GameIsRunning)". Переменную "GameIsRunning" устанавливают в 1 (истинно),
затем, если пользователь нажмет VK_ESCAPE, переменная устанавливается в 0, что приводит к
завершению цикла и игры. Пока что мы просто используем CONTROL-C для выхода.
*/
} // Конец программы.
/* *ПРОВЕРКА ОШИБОК*
Вы увидите, что при выходе за границы экрана происходят странные вещи. Это потому что мы не
проводим проверку ошибок. Я решил не делать этого для того, чтобы обучалка была меньше.
Я всегда любил код, где показан минимум, не скрытый множеством проверок. Разумеется вы
должны знать, как проверять ошибки. Вот что вы должны для этого сделать:
Проверить эту программу легко. У вас есть координаты x и y, верно? Итак, мы знаем что
наше окно имеет ширину 80 и высоту 25. Все что нам нужно сделать - это увеличивать значение х,
(то есть Position.x), только если х меньше 80, и уменьшать х, если он больше нуля.
То же и для значения y. Уменьшайте y, когда он больше 0, и увеличивайте, когда он меньше 25.
Вы скажете - а что если консольное окно не всегда имеет размер 80 на 25?. Есть хорошая
функция GetConsoleScreenBufferInfo(). В нее нужно передать буфер вывода (HANDLE hOutput) и
переменную типа PCONSOLE_SCREEN_BUFFER_INFO (PCONSOLE_SCREEN_BUFFER_INFO pConsoleInfo).
Это структура, содержащая переменные, в которых хранится информация о консольном окне,
начиная с атрибутов окна и заканчивая его размерами.
* Проблемы с WinNT *
Вам следует помнить, что код для мыши не работает на некоторый операционных системах, таких
как Windows 2000, до тех пор, пока вы не отключите для окна QuickEdit и Insert Mode. Чтобы
сделать это, запустите программу и щелкните на верхнем левом углу окна. Появится меню,
зайдите в Свойства. Убедитесь, что у вас выбраны Опции и посмотрите на Редактировать опции.
Отключите Быстрое Редактирование и Режим Вставки. Таким образом программа не подумает, что
вы хотите скопировать текст из окна. Затем, нажмите Сохранить и убедитесь, что сохранение
произойдет для всех заголовков с таким именем. Закройте программу и запустите вновь. Она
должна заработать.
*/
// © 2001 GameTutorials
// Перевод © 2004 Евгений Казеко
// www.gamecoder.kazeko.com
// evgeniy@kazeko.com
|