Здравствуйте! Хочу представить вам свой первый движок для создания 2D игр. В нем можно рисовать текст и картинки, и многое другое
Что это такое? WaterLemon2D Engine – open-source движок для создания 2D игр на C++, написанный мной с использованием DirectX11. Список того, что этот движок поддерживает: отрисовка изображений, управление вводом с мыши и клавиатуры, проигрывание звуков, сеть и прочее. Движок разделяется на подсистемы: 1. Подсистема графики – класс Graphics, отрисовывает картинки и текст с помощью Direct3D. 2. Подсистема ввода – класс Input, перехватывает нажатия клавиш и мыши с помощью DirectInput. 3. Подсистема звуков – класс Sound, проигрывает звуковые файлы с помощью DirectSound. 4. Подсистема времени – класс Time, подсчитывает время и FPS (частоту обновления) с помощью стандартных функций Windows. 5. Подсистема сети – класс Network, осуществляет передачу данных между компьютерами в сети с помощью WinSock.
По каждой из этих подсистем будет небольшой туториал
В папке Tutorials содержатся туториалы по движку, я их копирую сюда.
Это первый туториал по WaterLemon2D, где мы научимся выводить изображения и текст, а также познаем основы этого движка.
Для начала нужно скачать архив с папкой WaterLemon2D, где содержится исходный код движка и WaterLemon2D.lib. Примечание: В движке используется DirectX11, и все проекты я пишу на Visual Studio 2012, движок в том числе писался на нем, поэтому информация о линковке библиотеки внизу подразумевает, что у читателя установлен последний DirectX SDK и Visual Studio какой-нибудь версии. Создаем проект (Файл - Создать проект), выбираем вкладку Visual C++. Выберите «Проект Win32» либо «Консольное приложение Win32». Тип проекта не играет особой роли, но я выбираю «Проект Win32», так как там отсутствует консолька, которая совсем не нужна движку Теперь заходим в «Проект – Свойства проекта» (Либо жмем Alt + F7), в самом верху ставим «Конфигурация – Все конфигурации» заходим в «Свойства конфигурации – Каталоги VC++», добавляем в «Каталоги включения» папку Include из DirectX SDK (у меня такой путь до нее: C:\Program Files\Microsoft DirectX SDK %28June 2010%29\Include), а в «Каталоги библиотек» добавляем папку Lib\x86 из того же SDK. Также не забудьте добавить папки Include и Lib из папки движка В папке движка есть папка Sources, программа ищет содержимое этой папки (шейдеры и текст) при запуске, поэтому эти файлы должны быть в папке с игрой.
Теперь создаем файл .cpp, называем его как-нибудь (например, main.cpp) и начинаем писать в нем код:
У меня есть 2 варианта библиотеки – один для дебажной версии, другой для релизной. Этот код означает, что если при компиляции указана Debug-конфигурация, то подключаем дебажную версию движка, иначе – релизную. Теперь дальше:
Код
#include <WaterLemon2D.h>
Подключаем главный файл движка, который, в свою очередь, подключает другие файлы подсистем. Дальше – объявляем пустые функции GameStart() и GameUpdate():
Код
void GameStart() {
}
void GameUpdate() {
}
Первая функция каждый раз будет вызываться по началу работы приложения, вторая – каждый кадр, пока приложение работает. Пока эти функции пустые. Идем дальше:
Первой строчкой идет объявление главной функции приложения. Но для консольных приложений вместо этой строчки напишите просто int main(); Дальше идет блок try-catch. Что это означает? Если попадется какая-нибудь ошибка (не получилось загрузить звуки, инициализироваться, и прочее), то будет создано исключение WaterLemonException, которое будет обработано (вывод информации об ошибке и прекращение работы). Что означает System::Get()->Run()? Это bool функция, которая ведет весь процесс управления приложением, пока она работает и возвращает true, работает и функция GameUpdate(), а если произойдет закрытие приложения, то блок try-catch просто закончится и приложение возвратит 0, за исключением исключений. Почему я использую такую странную конструкцию – System::Get()->Func(…)? Потому что в движке я реализовал шаблон «Одиночка» (Singleton) для всех важных классов, так как это было для меня удобнее всего. Если хотите узнать, как этот шаблон реализуется, добро пожаловать в Википедию. Теперь переходим к функции GameStart() и пишем в ней: System::Get()->Init(L"DEMO VERSION",1200,750,false); Эта функция инициализирует окно приложения и эта функция обязательно должна быть у любого приложения на этом движке. Первый аргумент функции – заголовок окна. Так как вместо массива символов (char*) для инициализации окна требуется тип LPCWSTR (или const WCHAR*), то перед строкой стоит знак L. Второй и третий аргументы – ширина и высота экрана, четвертый – булева функция, которая говорит, включить полноэкранный режим либо нет.
Переходим к функции GameUpdate():
Код
Graphics::Get()->BeginScene(1.0f, 0.0f, 0.0f); // Красный цвет по шкале RGB Graphics::Get()->EndScene();
Скомпилируйте и запустите проект. Для чего нужны эти функции? Во-первых, важно понимать, как работает отрисовка. Допустим, вы управляете игроком клавишами. Игрок передвигается и каждый кадр рисуется на новой (или прежней) позиции. И тут появляется проблема: если все время рисовать игрока, то после движения игрока останется след из спрайта игрока, так как спрайт игрока постоянно рисуется, а прошлые отрисовки спрайта никуда не деваются. Поэтому каждый кадр сцена очищается в какой-либо цвет (обычно черный, хотя в моем примере очистка идет в красный свет), спрайты рисуются заново и сцену предоставляют на монитор, так что вся отрисовка проходит между этими двумя функциями.
Функция во второй строчке загружает изображение, название файла которого введено в качестве первого аргумента (да, это тоже const WCHAR*), второй и третий аргументы определяют размер рисуемой картинки, а четвертый и пятый аргументы – ее позицию. Функция в третьей строчке – перегруженная, она рисует часть картинки, в то время как предыдущая функция рисует ее полностью. Первые пять аргументов такие же, как в предыдущей функции, последующие два аргумента устанавливают точку, где начинается отрисовывание, и 2 последних аргумента устанавливают точку, где отрисовывание заканчивается, таким образом, отрисуется только часть картинки 60 на 60 пикселей. Примечание: я загружаю картинки формата .dds, что это такое? DDS расшифровывается как Direct Draw Surface, то есть это родной формат DirectX. Но отрисовать можно не только картинки этого формата, но еще PNG, BMP, GIF(Не анимация ), JPG, TIFF и, возможно, еще какие-либо малоиспользуемые форматы, причем прозрачность поддерживается в форматах PNG (только 8-битные картинки), GIF и DDS. Четвертой строчкой рисуется текст «Hello!» в позиции (600,700) белого цвета (1.0,1.0,1.0).
В принципе, это все, что нужно знать для отрисовки графики в WaterLemon2D
Вот список используемых функций класса Input:
Код
bool GetKeyPressed(byte key); // Нажата ли клавиша? bool GetMouseKeyPressed(byte key); // Нажата ли кнопка мыши? POINT GetMouseLocation(); // Получаем позицию мыши
Первая функция возвращает true, если клавиша нажата, и false, если нет. Так как я для системы ввода использовал DirectInput, то в качестве аргумента мы в эту функцию вносим название клавиши, начинающееся на DIK_... (Direct Input Key), например, DIK_A, DIK_Q, DIK_ESCAPE и DIK_RETURN означают соответственно клавиши A, Q, Esc и Enter. Вторая функция возвращает true, если нажата указанная кнопка мыши, и false, если наоборот. В качестве аргумента вносим значение 0 или 1; 0 – левая кнопка мыши, 1 – правая кнопка мыши. Третья функция возвращает позицию мыши как объект POINT. POINT это структура от Microsoft, которая ничего интересного не делает, а только содержит поля x и y. Если мы захотим получить позицию мыши по координате x, то получим ее так: Input::Get()->GetMouseLocation().x
Вот пример – код, который завершает работу приложения по нажатию кнопки Escape.
Код
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int) { try { System::Get()->Init(L"Game",350,200,false); while(System::Get()->Run() && !Input::Get()->GetKeyPressed(DIK_ESCAPE)); // Будем постоянно вызывать Run(), пока мы не закроем приложение либо не нажмем Escape } catch(WaterLemonException ex) { MessageBox(0,ex.text,L"Error",MB_OK|MB_ICONSTOP); return -1; }
Первая функция загружает звуковой файл filename. Если во втором аргументе указано true, то звук будет воспроизводиться бесконечно, иначе – только один раз. Вторая функция делает то же самое, что и первая, только воспроизводит звук только один раз. Примечание: движок умеет загружать только wave-файлы Третья функция прекращает произведение звукового файла filename, ранее загруженного.
Пример использования:
Код
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int) { try { System::Get()->Init(L"Game",350,200,false);
Первая функция возвращает частоту обновления кадров. Вторая функция возвращает процент «нагруженности» центрального процессора, может быть, пригодится Примечание: Вторая функция на некоторых компьютерах возвращает всегда 0, так как Windows не позволяет взять эту величину из реестра. Странно. Третья функция возвращает время с начала работы приложения в миллисекундах
В этом туториале мы научимся передавать данные между приложениями, напишем эхо-сервер. Перед началом работы важно понять, как передаются между собой данные, понимать, как работает приложения сервера и клиентов. Я не могу описать взаимоотношения этих приложений в силу ограниченности вводного курса, поэтому за ответом на эти вопросы отсылаю вас в Google или Wikipedia. Для тех, кто что-нибудь смыслит в программировании сетей – сеть в движке основана на архитектуре клиент-сервер с использованием протокола TCP/IP. Все, что делает сервер – регистрирует сокет на компьютере для прослушивания сетей на определенном порте, работая с каждым клиентом по отдельности, а все, что делает клиент – подключается по сети к серверу, используя IP компьютера, где он стоит, и порт, который он использует. Самое важное, что надо понимать – клиент и сервер рассылают друг другу массивы символов, причем клиент может их отослать только серверу, а сервер – всем своим клиентам. Примечание: по правде говоря, это не так для приложений клиент-клиент, где приложение одновременно и сервер, и клиент, но движок не работает с такой архитектурой. Сеть в движке представляет класс Network. Вот его функции:
// Клиент void Connect(char* ip); void Receive(char* buff, int size); void Send(char* buff, int size); void SetReceiveHandle(void (*func)());
// Сервер void BindAndListen(); void SetClientHandle(void (*func)(int)); void Receive(int number, char* buff, int size); void Send(int number, char* buff, int size); int GetClientCount();
Функция Init инициализирует WinSock, используя указанный порт. Также в качестве второго аргумента нужно передать true, если приложение асинхронное, и false, если оно синхронное. Дело в том, что функции в WinSock, которые принимают данные, останавливают работу приложения до тех пор, пока данные не придут. Этот подход нормален для крестиков-ноликов, но в остальных случаях этот подход не оправдан, так как вместе с приемом данных следует рисовать графику, проигрывать звуки и делать прочие вещи. Поэтому, если вы пишете клиент, вам нужно писать true, а если сервер – false. Функция Close чистит ресурсы, созданные при работе с WinSock. Теперь функции клиента. Connect() создает связь с сервером, если он есть на компьютере с указанным IP. Receive() сохраняет данные в массиве символов, причем данные размером не больше size (примечание – один символ занимает один байт). Send() отправляет данные из массива символов, отправляется кол-во байтов не больше size. SetReceiveHandle() принимает функцию, которая будет вызываться каждый раз, когда поступили данные (таким образом, в той функции Receive() проходит почти мгновенно, без ожидания, так как данные уже «стучатся в окошко»). Позже я на примере покажу подробнее, как это должно работать. Функции сервера. BindAndListen() заставляет сервер принимать клиентов. Эта функция (в основном потоке) продолжается бесконечно, пока сервер не закроют либо не произойдет что-нибудь страшное. SetClientHandle() принимает функцию, в котором обрабатывается клиент. Когда к серверу подключается клиент, для него создается отдельный поток, внутри которого выполняется эта функция. Receive() и Send() выполняют то же самое, что у клиента, но используя номер клиента. «Что-то?» Дело в том, что основной объект WinSock – сокет, который служит для передачи данных. Так вот, у клиента сокет один для связи с клиентом, и поэтому заранее известно, по какому сокету следует отправлять и принимать данные, а у сервера их несколько – один для прослушивания клиентов, а остальные – для клиентов, которые записаны в список. Когда создается новый сокет клиента, он заносится в список, а функции отправляется индекс сокета клиента в списке, по которому отправляются и принимаются данные. Вы увидите в примере, как это делается. Последняя функция – GetClientCount() – возвращает количество клиентов.
Сейчас мы напишем эхо – сервер. Этот сервер принимает данные от клиента и сразу же отправляет их назад. Клиентом будет отдельное приложение. Код сервера:
Хмм красивенько, но мне кажется, или для 2D не стоило брать DirectX 11 (ну можно 9 / 10 или вообще OpenGL) В общем удачи тебе с твоим движком) Занимаюсь программированием на PHP, JavaScript (jQuery), C# (не Unity3d!), Action Script 3.0 (в основном клепаю игрушки под соц сети.), Node.JS Недавно стал изучать Python.
К сожалению, нет, так как мой костыль еще слишком ограниченный для того, чтобы создавать проекты с максимальной быстротой. К примеру, шрифт используется только один, в туториалах иногда проскакивают прорехи вроде "cout" и "DIK_ESCAPE", когда более серьезные движки используют свою реализацию класса строк и enum'ы, к тому же один тоуарищ намекнул, что не комильфо, когда на нем можно писать игры онли виндовс с новыми видеокартами. Это не означает, что я бросил свой двиг, просто нужно время для выхода на более новый уровень.
Сообщение отредактировал Izaron - Четверг, 29 Августа 2013, 13:47
Глянул уроки, отличный двиг, достаточно простой. Мне бы его, да на годик раньше. А сейчас у меня уже другие потребности. В общем, удачи, так держать! Параноик с гениальным планом по захвату мира.