Для начала, всё ниже описанное лишь мой опыт, в целом это просто хобби. Я слишком странно могу описывать некоторые вещи, ибо пишу нечто подобное первый раз. Надеюсь всем всё будет понятно (=_=)
Это не тутор, а скорее моё виденье проблемы «для чайников». Ниже я лишь опишу самые вводные в тему, и наведу на мысли что и как может делаться. На полноту не претендую, поэтому самых умных жду в комментариях с пожеланиями и угрозами.
АТЕНШОН, МНОГА ТЕКСТА НЕТ ПИКЧ
Предостережение
Реклама своей игры как «написанная на собственном движке» это интересная затея, но явно не оправдывает себя, если твоя цель именно сделать игру, а не развлечься.
Разработка полноценного движка, о котором так мечтают люди начавшие свой путь в гейдеве, это время затратная вещь. Любой игровой движок состоит из компонентов основных на каких-то абстракциях, и разница между «сильным» и «слабым» инструментом — в количестве этих компонентов.
Поясню — что бы написать минимально рабочую игру, тебе нужно: обработка ввода, вывод графики и игровой цикл с логикой. Можно сказать — «всего 3 компоненты и ты уже можешь делать свои крестики-нолики или тетрисы!». Но для звания «великого игрового движка», при мысли о котором ты возбуждаешься, явно не хватает всего 3х компонент. Различные механизмы подгрузки данных, гибкие системы рендера, система скриптов, встроенные редакторы уровней — всё это требует времени на реализацию, и чем больше ты хочешь, тем больше времени у тебя уйдёт на разработку «движка с 0», а сколько времени потратится на написание самой игры?
Будь реалистом, и подумай над тем, что бы написать свою игру на уже имеющихся инструментах. Это сэкономит тебе кучу времени и добавит пару галочек в твоё портфолио. Не бойся тратить деньги на коммерческие движки, если от этого зависит как быстро ты сможешь выйти на рынок. (есть риск что не окупится)
А как писать?
Для начала нужно подвести черту между двумя кардинально разными подходами в написании этих ваших «движков». Я обозвал их так…
- «Динамические движи», это те в которых вся игровая логика содержится внутри объектов, которые можно передать в другие бинарные модули. (Прим — .dll библиотеки)
- «Статические движки», это те в которых вся логика содержится внутри самого движка и определяется на этапе компиляции.
Какие нюансы я обнаружил при такой классификации?
«Статические движки»
- Проще в написании и требуют минимального порога вхождения.
- Весь код собирается один раз, из-за чего компилятор может провести оптимизации, нужные конкретно для описанной логики.
- Имеются хорошие инструменты «отладки» для многих базовых библиотечных функций.
- Нет возможности модифицировать собранную игровую логику. Что решается понятием «скриптов», которые используя логику игрового движка, способны расширить уже описанную игру. (Самая геморная часть)
«Динамические движки»
- Требуют глубокого понимания матчасти, из-за виртуализации объектов.
- Код можно разбить на независимые модули и подключать буквально на лету. В некоторых играх есть моды которые реализуют «движок в движке», которые используют другие подключаемые модули.
- Большинство инструментов для отладки придётся писать самому, если это не встроено в сам язык. То есть, если ты не пишешь на .NET/JAVA/Lua?
- «Скрипты» могут быть реализованы в виде скомпилированных бинарников обёрнутого в объекты, от чего «потанцевал» при добавлении контента может быть больше (в плане производительности).
Большинство «индюшачьих» игровых движков описаны как «статические». Их гораздо проще писать и ещё проще поддерживать. Использование «динамического» подхода, даст тебе ненужную модульность. Возможно написание своего крутого интерфейса для аллокатора памяти хорошая затея, но ты бы мог потратить это время делая игру, а не натирать цепь разобранного велосипеда.
*** Рекомендация — пиши «статику»
*** Самый наивный способ сделать «динамику» — писать классы с виртуальными методами или их аналогами.
Кроссплатформенность
Это боль. (вся глава моё нытьё…)
Первая мысль которая должна посетить твою голову — как распространить игру на максимально широкую аудиторию.
Ты можешь использовать кроссплатформенные библиотеки, но их функционал крайне ограничен, так как они пытаются создать абстракцию которая «будет жить на любой платформе». Ты конечно можете написать свою библиотеку… Но вот в чомъ мем
Проблема в том, что платформы достаточно сильно отличаются даже в самых базовых вещах. Как пример — Windows при старте программы не имеет потоков ввода-вывода и для этого ей нужно создавать консоль, но консоль можно создать 2 различными способами и у каждого способа есть куча параметров, для лююююбых потребностей. В Linux же всё проще, у тебя со старта приложения есть потоки ввода-вывода и что бы «увидеть их» нужно просто перенаправить их в файл или создать окно-терминал. КАК ты даже ТАКУЮ простую проблему будешь решать, я не представляю. (я это уже сделал)
Чего уже говорить о создании графического контекста…
Вот ты пытался в OpenGL на Windows? Знаешь что на официальном сайте написано использовать GetDC() при связывании контекста окна и OpenGL? А ты знал что это DC на самом деле может быть принтером? Может быть целым монитором? Видеокартой? Просто окном? «Ресурсом менеджера окон»? А то что в Linux ты должен ручками выбрать нужный тебе монитор для запуска? Всякие драйверы-сервера запускать? И не один. И это надо как-то собрать в одну единую абстракцию и непринуждённо дёргать по воле случая…
Не страдай хернёй, я просто пытаясь понять как это всё работает не одну неделю убил —> используй готовые библиотеки. Их качество оставляет желать лучшего, но это избавит тебя от большой части головняка, который ты можешь себе вообразить, и позволит тебе сконцентрироваться на написании самой игры. (не исключается наличие багов в самих библиотеках)***
И всё же… Написать(наговнокодить) хотя бы простенькое приложение на API платформы, как по мне, важно. Так, ты будешь лучше понимать, что в конкретной системе значат абстракции которые дают фреймворкиязыки.
Делаем игры, наивно
В начале статьи я ввёл понятие «компонента». На самом деле я называю так любую штуку «которая делает что-то другое» )))
Любой код можно разбить на некое число компонент-модулей. Самое примитивное приложение состоит всего из 3 модулей
- Обработка ввода
- Вывод изображения
- Логика (зачастую на стейт машинах)
Уже это позволяет писать тебе довольно простые игры. Скажем крестики нолики или «давилку мух»? Давай вместе представим как оно может работать.
- Загружаем нужные ресурсы
- — В цикле…
- Рисуем: фон > картиночки
- Обрабатываем ввод
— Если нажата мышка, проверяем хитбоксы объектов
— Если прошли проверку, меняем «игровое состояние» - (для «давилки мух») Раз в Х циклов запускаем новую муху по некоторой траектории. И обрабатываем перемещение живых мух.
И это уже можно назвать игрой. «Погоди» — скажешь ты — «Но фактически тут 4 модуля, загрузка ресурсов!!». Хах, но если ты чуть-чуть разбираешься в линковке приложения, то ты можешь вшить данные в бинарник) И тебе не нужно будет загружать никаких данных. Поведение мух, также, вшито в движок и относится к логике, потому фактически тут именно 3 модуля.
Но это не будет игровым движком, а скорее просто игрой. Поэтому надо выделить чем именно должен заниматься движок.
Я бы сказал так — «движок занимается обработкой пользовательских данных и может иметь изменяемую логику»
Из такого определения давай же опишем некий «сферический движок для платформера».
Для начала, тебе потребуется описать больше чем один модуль, а логика самого движка будет гораздо сложнее.
- Конкретная загрузка ресурсов
- Обработка ввода, который конвертируется в абстракции
- Вывод изображения, который обходит множество объектов
- Игровая логика, которая уже разбита на несколько частей
— Обработка загрузки-переходов уровней
— Информация о сцене
— Нахождение коллизий и проверки на триггеры
— Обработка поведения ИИ?
— Обработка ввода от игрока
— Скриптовый процессор
Теперь, как прошлый раз, давай разберём поэтапно, что и как движок должен делать.
— Для начала опишем «минимальную» логику…
- Информация о сцене (уровне)
— Какой фон
— Набор тайлсетов (2D матрица)
— Предметы
— Юниты
*** Всё кроме фона должно иметь информацию о себе, типа — «текстура», «какие-то свойства», есть ли «триггер», «скрит» или «поведение» - Вшитые в движок «примитивы»
— Триггеры (какие бывают, что проверяют, что вызывают. прим — если хибоксы пересекаются — запускает смену уровня)
— Поведение (хардкодное поведение, что делает объект. прим — триггернутый предмет увеличивает свойство Х, у объекта который триггернул. Объект Х движется в одну сторону и случайное время меняет направление. Это можно оформить в виде «функций» для скриптового языка. )
— Свойства (Что за свойства, определяемые скриптами свойства)
— Процессор скриптов - Обработка передвижений-триггеров-физики?
Ну и сам игровой цикл будет состоять примерно из такой логики
- Загружаем информацию об уровне (описание уровня)
- Загружаем нужные ресурсы (текстуры и скрипты)
- — В цикле…
- Рисуем: фон > тайлест > предметы > юниты
- Обрабатываем ввод
— Абстрагируемся от кнопок и просто передаём «Идти влево» - Обрабатываем логику
— Проходимся по всем единицам со скриптами
— Проходимся по тем, у кого базовое поведение
— Что-то делаем с игроком?
— Проверяем коллизии-физику? (вызываем триггеры если что-то нашли)
— Двигаем юнитов-предметы
Тут нету анимаций… А первое время лучше не трогать скриптовый процессор, а задавать поведение явно. Не зря же мы добавили отдельный модуль который занимается именно этим?
И… Вот примерно в таком виде это уже можно назвать своим полноценным движком! Ахаха… Можно постоянно добавлять функционал, добавить графические эффекты, анимации или сделать меню с кнопками. В какой-то момент ты поймёшь, что хардкодить объекты не самая лучшая затея и перепишешь свой код, который будет использовать какие-то обобщённые абстракции. В целом, писать не так уж и много, не так ли?
Поясню за скрипты — для многих это будет открытием, но скрипты можно делать и в виде виртуальной машины-процессора. Твоя задача будет — написать парсер текста, описать виртуальную машину и придумать как именно получать информацию о сцене-объектах. В качестве примера, можно использовать многострадальные стековые процессоры, по типу forth. Они очень легко делаются, но к их логике нужно привыкать. По сути это единственный выход написать «быстро» ваш собственный «скриптовый процессор».
*** Обязательно сделай вывод логов при сборке скрипта, или при его работе.
*** Старайся избегать логики типа [INT a += «Lord»]. Писать нетипизрованный код опасно, но можно выделить отдельные команды для работы с конкретными типами.
*** ДА, ТЫ БУДЕШЬ ПИСАТЬ СКРИПТЫ НА АССЕМБЛЕРЕ.
*** Для написания простого ассемблера, хватит и знаний типа — Sting.indexof(«ADD»); и подобного говнокода. Но что бы написать нормально, или хотя бы простенький язык, вам нужны знания о «регулярных выражениях» или «парсерных комбинаторах».
*** Не надо упарываться в «полноценный язык», посмотрите как писались языки программирования в бородатых 80х, даже тот же Pascal. Они работают просто и честно, такие реализации займут у вас в разы меньше времени, чем описание «очередного» ???CRustHaskell???
Типа канец?
В целом написать некий «движок в вакууме» не такая сложная задача, как кажется. На ранних этапах большинство поведений-абстракций можно вшивать в движок. Да и в целом, большинство вещей работают довольно просто, и требуют от вас лишь знаний и понимания. Но я напомню, написать свой движок для игры — плохо. Это отнимает огромное количество времени, которое вы можете потенциально потратить на разработку самой игры. А любая гордость проходит, после осознания того, что ты делал свою игру примерно 20% времени пока писал код.
Изначально, решился написать эту статью, ради привлечения инвестиций, на время разработки своей «базовой +18 новеллы». (инициатива друга)
Так что ты это, кнопочку то нажми, а?
В целом, если попрёт, у меня есть уже более конкретные истории с чем я сталкивался и как я решал какие-то проблемы. Я в основном концентрируюсь на низкоуровневых абстракциях, отдавая на откуп всю логику подключаемым модулям, которыми управляет специальный планировщик. Потому и проблемы у меня соответствующие. И в качестве примера…
Вот первое что пришло в голову, из самого простого…
- Как лучше работать с памятью-данными.
- Проблемы STL библиотек. (C++)
- Детали виртуализации объектов, микро рекомендации.
- Как можно делать «моды».
- Профилирование и Логирование, почему это важно.
- Что нужно знать о многопотоке, вводные-подводные.
- Асинхронно или параллельно? Как это работает?
- Работа с текстом-кодировками, и почему это настоящий ад.
На счёт моего кода — не будет опенсорса. Когда я релизнусь, я выложу лишь API к бинарнику. По сути, я использую «динамический» подход, который описывал выше. Потому запустить свою игру, можно будет просто собрав .dll и запустить бинарник запихнув либу в аргументы строки. Но вот когда это случится… Когда я перестану морить себя голодом? Кто знает
PS — Под конец получилось немного сумбурно, ибо я писал всё за один заход. В целом я описал лишь поверхностно многие вещи. Если будет спрос, могу углубится.
PPS — Мне тут говорят что бы я сделал бусти и публиковался впредь там, раз в месяцок публикуя «фри контент». Может в следующий раз? Я просто не знаю о чём там можно писать, лол.
PPPS — «Статья слишкам длинная устал читать, пиши кароче»
Так что… Ещё свидимся?
Download Article
Download Article
- Programming Basics
- Planning
- Development
- Completion
|
|
|
You can create a game engine to simplify the programming process for all of the games you make. If you’re a developer looking to create your own game engine from scratch, check out this tutorial for coding your own a simple game engine!
-
1
Choose your Language. There are a variety of languages from which programs are made. The language you choose doesn’t matter too much, but the most important thing is that you start somewhere.
- There are many programming languages to choose from, but most go with C++ or Java and they are also the most useful in Game Development.
- Once you learn one language, it’s much easier to learn another.
-
2
Find a course. The most effective way (in my opinion) to learn programming/computer science is to take a class! Whether this is a class at your school, or outside of school shouldn’t matter.
- No matter who you are, you can find a programming class that suits you.
- MIT OpenCourseWare (http://ocw.mit.edu/) has a variety of free classes.
- If you look around on Google, you’ll find a number of other sites that also have free lectures and classes available.
- You could also have a friend teach you a language, sharing is caring.
Advertisement
-
3
Practice. You don’t want your first game to be you big, important game. You want a chance to screw up and not care too much about your project.
- Try to make a simple game.
- If you learned Java, check out the Swing package.
- Don’t worry too much about this project(s), they should only take up a few weeks of your time.
- Learn from your mistakes.
Advertisement
-
1
Think of a Game Idea. Try to challenge yourself. That way if you don’t achieve all you set out to, you’ll still (probably) have a pretty good game left over.
- Think for a while, don’t feel pressured to do this in one sitting.
- Sit on your idea for a while so you know it is good.
-
2
Formalize your Idea. Technical communications is important in any kind of engineering, including software engineering. You don’t want to tell someone that you’re making an apple and they go and make you audio for a pear.
- Write up a «Game Design Document». These are used in professional game development, but more importantly, they easily communicate your idea(s) to others. There are many free templates available online.
-
3
Recruit Help. You don’t have to go at this alone. It’s also more fun and exciting in a group.
- You can’t make a custom game engine and manage the project without help.
- Ask your Friends first before going to strangers or advertising for help, you’d be surprised who would love to get into the game industry.
Advertisement
-
1
Research. Look into what you’re going to do before you do it. Even when you make an engine from scratch, there are still a number of tools that you could make your engine out of.
- Look into «OpenGL» if you learned C and «JOGL» if you learned Java.
- Maybe buy a textbook on OpenGL, «Redbook» is the most famous one, but it is online for free.
-
2
Draw Something. Render a primitive or 2D object to get started.
- Make a 2D triangle, or a cube.
- Look into «Display Lists» so you can draw many primitive objects.
-
3
Make Perspective. There aren’t many games where you can’t change where you’re looking.
- Make the perspective of your game (First person perspective, top-down, etc.)
-
4
Move Around. One step at a time! Except not actually because stepping is actually kind of complicated.
- Either move everything around the camera or move the camera view port, but they are the same to the processor.
- Be able to move in all angles, not just along the axis.
-
5
Add Textures (Images). That default color will get old after a while, and not many games are used with only solid colors.
- Splice them into your display list(s).
-
6
Add Audio. This makes your game much more interesting and realistic.
- Perhaps footsteps for when you walk.
-
7
Add Lighting. This also adds to the realism.
- Learn the different kinds of lighting.
- Use a sphere instead of a cube to make sure the lighting is working.
- You could put a primitive object where the light should be coming from to debug. Just make sure the light can get out of the box/sphere you put it into.
-
8
Add Collision Detection. The biggest thing people notice when you show them an incomplete game engine is the lack of proper collision detection.
- Make it impossible to walk through the cube.
- Make it possible to move (in other directions) when you are colliding with the cube.
-
9
Add Gravity. Most games have falling things somewhere.
- Make a floor, and jump around on it.
Advertisement
-
1
Finish Your Game. Don’t forget to market it. You may want to enlist a marketer (friend) to help you. Assume your game is going to be a hit so that way you can work towards that.
- Have Fun!
-
2
Manage Other Games. Don’t be coy, tell other developers that you made a game engine. You don’t have to be the only one to develop with your engine. When you let other people use it, you have the right to some of their royalties, but also you get constructive feedback and perhaps improvements to your engine.
- Game Engines are valuable and impressive.
- Did you notice how much those other engines are charging indie developers? (You could be that engine!)
- Use your engine to get wannabe game developers into the industry!
-
3
Good Luck! Start your journey into the booming game industry!
- Now you can laugh at your friends who said «Unity was easier».
Advertisement
Ask a Question
200 characters left
Include your email address to get a message when this question is answered.
Submit
Advertisement
Thanks for submitting a tip for review!
About This Article
Thanks to all authors for creating a page that has been read 83,778 times.
Did this article help you?
Download Article
Download Article
- Programming Basics
- Planning
- Development
- Completion
|
|
|
You can create a game engine to simplify the programming process for all of the games you make. If you’re a developer looking to create your own game engine from scratch, check out this tutorial for coding your own a simple game engine!
-
1
Choose your Language. There are a variety of languages from which programs are made. The language you choose doesn’t matter too much, but the most important thing is that you start somewhere.
- There are many programming languages to choose from, but most go with C++ or Java and they are also the most useful in Game Development.
- Once you learn one language, it’s much easier to learn another.
-
2
Find a course. The most effective way (in my opinion) to learn programming/computer science is to take a class! Whether this is a class at your school, or outside of school shouldn’t matter.
- No matter who you are, you can find a programming class that suits you.
- MIT OpenCourseWare (http://ocw.mit.edu/) has a variety of free classes.
- If you look around on Google, you’ll find a number of other sites that also have free lectures and classes available.
- You could also have a friend teach you a language, sharing is caring.
Advertisement
-
3
Practice. You don’t want your first game to be you big, important game. You want a chance to screw up and not care too much about your project.
- Try to make a simple game.
- If you learned Java, check out the Swing package.
- Don’t worry too much about this project(s), they should only take up a few weeks of your time.
- Learn from your mistakes.
Advertisement
-
1
Think of a Game Idea. Try to challenge yourself. That way if you don’t achieve all you set out to, you’ll still (probably) have a pretty good game left over.
- Think for a while, don’t feel pressured to do this in one sitting.
- Sit on your idea for a while so you know it is good.
-
2
Formalize your Idea. Technical communications is important in any kind of engineering, including software engineering. You don’t want to tell someone that you’re making an apple and they go and make you audio for a pear.
- Write up a «Game Design Document». These are used in professional game development, but more importantly, they easily communicate your idea(s) to others. There are many free templates available online.
-
3
Recruit Help. You don’t have to go at this alone. It’s also more fun and exciting in a group.
- You can’t make a custom game engine and manage the project without help.
- Ask your Friends first before going to strangers or advertising for help, you’d be surprised who would love to get into the game industry.
Advertisement
-
1
Research. Look into what you’re going to do before you do it. Even when you make an engine from scratch, there are still a number of tools that you could make your engine out of.
- Look into «OpenGL» if you learned C and «JOGL» if you learned Java.
- Maybe buy a textbook on OpenGL, «Redbook» is the most famous one, but it is online for free.
-
2
Draw Something. Render a primitive or 2D object to get started.
- Make a 2D triangle, or a cube.
- Look into «Display Lists» so you can draw many primitive objects.
-
3
Make Perspective. There aren’t many games where you can’t change where you’re looking.
- Make the perspective of your game (First person perspective, top-down, etc.)
-
4
Move Around. One step at a time! Except not actually because stepping is actually kind of complicated.
- Either move everything around the camera or move the camera view port, but they are the same to the processor.
- Be able to move in all angles, not just along the axis.
-
5
Add Textures (Images). That default color will get old after a while, and not many games are used with only solid colors.
- Splice them into your display list(s).
-
6
Add Audio. This makes your game much more interesting and realistic.
- Perhaps footsteps for when you walk.
-
7
Add Lighting. This also adds to the realism.
- Learn the different kinds of lighting.
- Use a sphere instead of a cube to make sure the lighting is working.
- You could put a primitive object where the light should be coming from to debug. Just make sure the light can get out of the box/sphere you put it into.
-
8
Add Collision Detection. The biggest thing people notice when you show them an incomplete game engine is the lack of proper collision detection.
- Make it impossible to walk through the cube.
- Make it possible to move (in other directions) when you are colliding with the cube.
-
9
Add Gravity. Most games have falling things somewhere.
- Make a floor, and jump around on it.
Advertisement
-
1
Finish Your Game. Don’t forget to market it. You may want to enlist a marketer (friend) to help you. Assume your game is going to be a hit so that way you can work towards that.
- Have Fun!
-
2
Manage Other Games. Don’t be coy, tell other developers that you made a game engine. You don’t have to be the only one to develop with your engine. When you let other people use it, you have the right to some of their royalties, but also you get constructive feedback and perhaps improvements to your engine.
- Game Engines are valuable and impressive.
- Did you notice how much those other engines are charging indie developers? (You could be that engine!)
- Use your engine to get wannabe game developers into the industry!
-
3
Good Luck! Start your journey into the booming game industry!
- Now you can laugh at your friends who said «Unity was easier».
Advertisement
Ask a Question
200 characters left
Include your email address to get a message when this question is answered.
Submit
Advertisement
Thanks for submitting a tip for review!
About This Article
Thanks to all authors for creating a page that has been read 83,778 times.
Did this article help you?
Из песочницы, Разработка игр, Анализ и проектирование систем, C++
Рекомендация: подборка платных и бесплатных курсов PR-менеджеров — https://katalog-kursov.ru/
Game Engine
Проектируем, пишем, думаем рассуждаем, читаем и многое другое
Внимание: статьи содержат много костылей!
Всем доброго времени суток. Не так давно решил заняться разработкой 3D игрового движка, так как структурированной информации по этому поводу немного, решил создать серию статей, в которой постараюсь показать больше техническую часть, нежели теоретическую.
Сейчас отойду от темы и хочу кое-что сразу оговорить… Я не являюсь хорошим программным архитектором и Senior developer(ом). Мне 21 и я маленький амбициозный C++ middle developer, могу ошибаться и писать глупости.
Only a Sith deals in absolutes. Obi-Wan “Ben” Kenobi
Рад видеть замечания и предложения в комментариях.
Пожалуй, на этом я закончу вступительную часть и перейдем к делу.
Часть 1: Вступление
Во-первых, надо разобраться, в чем суть движка и зачем его писать.
Хм… И что же это?
Игровой движок
центральный программный компонент компьютерных и видеоигр или других интерактивных приложений с графикой, обрабатываемой в реальном времени. Он обеспечивает основные технологии, упрощает разработку и часто даёт игре возможность запускаться на нескольких платформах, таких как игровые консоли и настольные операционные системы, например, GNU/Linux, Mac OS X и Microsoft Windows.
Ссылка на Wiki
— Такс… Значит, просто написать пару классов мало?
Хорошие движки (UE, Unity, Cocos2D) состоят из пары сотен классов, нескольких подсистем и кучи менеджеров. Если конкретней:
- Графическая система
- Звуковая система
- Система для работы с сетью
- Менеджер процессов
- Менеджер задач
- Менеджер объектов
- Менеджер сцен
и многое другое…
И что же нам делать? Как, что, куда и зачем?
Самое первое и самое главное — разделить большую задачу на более мелкие и идти шаг за шагом. Маленькими, неуверенными, черепашьими шажочками.
Последовательность статей:
- Часть 1:
ЧеЧто это и зачем? (Вступление) . - Часть 2: Каркас (Начинаем писать код + архитектура).
- Часть 3: Логирование или запиши мне вот это, а это не надо.
- Часть 4: Работа с файлами, загрузчики и game assets.
- Часть 5: Внимание, будет много математики! (Графическая подсистема):
- От окна до камеры за 1 урок
- Модели, текстуры,
материалырановато еще. - А дальше свет, тени, слёзы и много-много математики, да ну ее в ***
Это только первые наброски, материала будет намного больше!
Что должно получиться
- Модульность. Слабая связность.
- Мультиплатформенность.
- Простота добавления нового функционала без изменения существующего кода.
- Использование разных графических библиотек: DirectX, OpenGL, Vulkan.
- Графические примочки: тесселяция, PBR, SSLR и много других
непонятныхнавороченных плюх. - Оптимизация рендеринга: отсечение невидимых граней, BSP деревья и прочая нечисть.
- Редактор уровней.
Для вводной статьи, думаю, хватит. В следующей статье мы напишем каркас для движка и задумаемся над взаимодействием систем и менеджеров, а также что туда должно входить. Жду ваших советов, материала и предложений
Также вот списочек материала для тех, кому интересно:
- Описание структуры игрового движка Banshee
- Многопоточная архитектура движка (не для новичков)
- Менеджер состояний (не самая лучшая статья)
Заявление об ограничении ответственности: я делал это не один — Мэтт и Девин также внесли большой вклад в этот проект. Кроме того, мы продолжим вносить улучшения, поэтому следите за обновлениями и не стесняйтесь вносить свой вклад на GitHub.
Код можно найти здесь: https://github.com/kaedenwile/UW-Graphics
Честно говоря, мы не знали, насколько легко / сложно на самом деле заставить графический движок работать. Оказывается, линейная алгебра относительно проста, а код довольно прост. Эта статья будет довольно общим обзором того, как мы подошли к проблемам кода с более глубоким погружением в линейную алгебру, которую мы использовали.
Код
Чтобы проект оставался чистым, мы разделили код на три отдельных модуля.
Модуль дисплея
Прежде чем мы начали, мы поняли, что для того, чтобы наш графический движок хоть как-то работал, нам нужно иметь возможность отображать изображение на экране. Теперь библиотека пользовательского интерфейса по умолчанию в Python называется Tkinter, у которой есть свои плюсы и минусы. (Главный недостаток, с которым мы столкнулись, заключается в том, что он блокирует основной поток, и некрасиво работать с потоками, хотя это в нашем списке потенциальных оптимизаций.)
Чтобы справиться со всем этим, мы создали модуль display
в нашем проекте, где находятся классы Screen
и Bitmap
. Screen
обрабатывает задачи окна, настраивает и отключает, а также отвечает на вводимые пользователем данные. Bitmap
принимает три точки и цвет и рисует соединяющий их треугольник заданного цвета (раньше это был обычный Python с Tkinter, но с тех пор добавили numpy и подушку для ускорения).
Модуль алгебры
Поскольку графический движок по своей сути содержит так много линейной алгебры, мы создали удобный модуль, который имеет несколько классов: Vec3
, Vec2
и Mat3
(матрица 3×3). На самом деле это не более чем оболочки для массива или массива массивов, но они реализуют перегрузку операторов и некоторые вспомогательные методы (подумайте о скалярном произведении, перекрестном произведении и умножении матриц), которые делают остальную часть кода намного чище. .
Модуль двигателя
Решая, как структурировать наш графический движок, я опирался на опыт работы с OpenGL, Apple SceneKit, Unity, Roblox и программу 3D-моделирования Blender. Результат наиболее точно соответствует структуре, которую использует Unity.
Самый фундаментальный объект в нашем движке — это Node
. Node
имеет три свойства: Mesh
, Transform
и Shader
, а также массив дочерних узлов. Mesh
— это объект, который содержит список точек (Vec3
) и граней (массив индексов трех точек, которые соединяет это лицо). Transform
кодирует, как масштабировать, вращать и перемещать этот узел и все дочерние элементы относительно родительского узла. И Shader
в настоящее время просто сохраняет цвет узла, но это в списке улучшений.
От Node
идет объект Camera
, который кодирует важную информацию о рендеринге сцены. И хотя Camera
происходит от Node, он не поддерживает Mesh
или Shader
— только Transform
. Он также имеет несколько других важных свойств: фокусное расстояние, ширину изображения, высоту изображения, ближнюю глубину и большую глубину. Ширина и высота довольно просты, фокусное расстояние мы рассмотрим в линейной алгебре, а ближняя глубина и дальняя глубина — это просто точки отсечки для «слишком близко» и «слишком далеко», в которых мы отбрасываем треугольники от рендеринга. — часть более крупного процесса, называемого отбраковкой.
Объект самого высокого уровня в движке — это Scene
. Scene
довольно прост — он просто содержит корень Node
, дочерним элементом которого должно быть все в сцене, и ссылку на Camera
, который следует использовать для рендеринга. У него также есть самый важный метод: render()
, который заставляет все отображаться на Bitmap
с использованием большого количества линейной алгебры.
Линейная алгебра
Что меня больше всего удивило, так это то, что линейная алгебра, которая заставила все работать, была действительно довольно простой. Хотя большинство людей забыли об этом, умножение матриц и векторов, которым изучают в старших классах, действительно все, что вам нужно, чтобы заставить этот движок работать.
Чтобы объяснить линейную алгебру, мы рассмотрим один вызов метода render()
в нашем Scene
объекте. Представим, что наша сцена содержит единственный узел со следующей сеткой:
И чтобы было интересно, почему бы нам не переместить его на (5, 20, 3)
, повернуть вокруг оси z на 45 ° и удвоить его размер. Используя стандартную камеру (фокусное расстояние: 2, ширина: 4, высота: 3), мы должны получить следующее изображение:
Шаг 1. Примените преобразование
Когда мы только начинаем, каждый отдельный узел определяет свое собственное пространство. Наш куб существует в другом пространстве, чем наша камера, которая существует в другом пространстве, чем любой другой узел. Чтобы увидеть, где на самом деле вершины находятся по отношению друг к другу, нам нужно применить масштабирование, поворот и перенос каждого узла ко всем его вершинам и дочерним элементам.
Во-первых, давайте посмотрим на сам объект Transform
. Для его создания мы предоставляем три вектора: перевод, поворот и масштаб. Поскольку действия поворота и масштабирования являются линейными преобразованиями, они могут быть описаны с помощью матриц и реализованы с помощью умножения матриц, поэтому код для Transform
просто содержит одну комбинированную матрицу для поворота и масштабирования. С другой стороны, преобразование отображает нулевой вектор в другое место, кроме нуля, поэтому это не линейное преобразование и не может быть описано матрицей. Вместо этого он просто держится за вектор трансляции.
Учитывая Transform
, довольно просто переместить вершину из одного пространства в пространство, описанное преобразованием — манипулировать указанным вектором с помощью матрицы преобразования, а затем смещать его вектором преобразования. Но что, если нам нужно выполнить преобразование для другого преобразования? (Представьте себе случай, когда у вас есть еще один узел, являющийся дочерним по отношению к нашему кубу — нам нужно применить как родительские, так и дочерние преобразования к вершинам дочернего элемента.) Что ж, оказывается, умножение матриц эквивалентно выполнению одного преобразования и потом еще один! Это означает, что мы можем создать новый Transform
, который учитывает как родительские, так и дочерние преобразования, умножая матрицы и складывая переводы.
Итак, чтобы переместить каждую вершину в единое мировое пространство, мы начинаем с корня и продвигаемся вниз к каждому узлу и каждой вершине, применяя и связывая преобразования по пути. Затем мы сохраняем большой список всех новых вершин и граней (важно обновить грани, чтобы они соответствовали новому индексу каждой вершины) и передаем его следующему шагу.
Шаг 2: сортировка по W-индексу
W-индекс — одна из основных частей растеризации, которая не является стандартной линейной алгеброй. Идея проста, но важна: мы хотим нарисовать ближайшие поверхности поверх самых удаленных поверхностей. Если в вашей сцене красивое голубое небо, которое находится очень далеко, мы хотим, чтобы дерево на переднем плане не было перезаписано визуализацией треугольников неба. Чтобы решить эту проблему, для каждого лица рассчитывается индекс w или расстояние до камеры. Лица с наивысшим индексом w рисуются первыми, а лица с самым низким индексом w рисуются последними и оказываются наверху.
Чтобы вычислить лицо, нам нужно найти центр лица и вычислить квадрат расстояния от этого положения до точки фокусировки камеры. (Мы используем квадрат, потому что извлечение квадратного корня затратно с вычислительной точки зрения и замедлило бы работу нашего двигателя.) Мы столкнулись с проблемами, когда использовали среднее положение точек в качестве центра, потому что центр в конечном итоге сдвигался бы к основанию треугольник, что иногда приводит к неправильному порядку. Теперь мы вычисляем ограничивающую рамку трех точек и используем ее центр, что отлично работает.
После того, как все w-индексы вычислены, грани переупорядочиваются в соответствии с убывающим w-индексом и переходят к следующему шагу.
Шаг 3. Рисование в растровое изображение
Теперь, когда у нас есть все лица, которые мы хотим нарисовать в мировом пространстве, и они упорядочены правильно, последний шаг — превратить трехмерный мир в двухмерное изображение, которое мы можем поместить на экран.
Мы делаем это, отображая каждую вершину из мирового пространства в 2D-пространство, определяемое камерой, которое мы называем экраном. Помогает представить нашу камеру такой:
Двухмерное положение вершины задается путем рисования линии между точкой фокусировки и этой вершиной в мировом пространстве, а затем вычисления ее пересечения с экраном. Затем мы превращаем это положение пересечения в 2D-координату. Все, что не пересекает экран (который имеет определенную ширину и высоту), считается вне поля зрения камеры и не отображается, как в реальной жизни.
Математически мы достигаем этого, находя два вектора: вектор от точки фокуса к вектору, который мы хотим нарисовать, который мы назовем a, и вектор от точки фокусировки к центру экран, который мы назовем f. Точка пересечения должна находиться в плоскости экрана, а это значит, что ее компонент вдоль f должен быть равен длине f. И поскольку он будет проходить вдоль линии, соединяющей точку фокусировки и нашу вершину, это будет скалярное число, кратное a. Если мы назовем вектор от фокальной точки до точки пересечения v,, мы получим следующую систему уравнений:
Решая для s, получаем:
Теперь, когда у нас есть положение точки пересечения в мировом пространстве, мы можем превратить его в экранные координаты, найдя вектор от центра экрана к точке пересечения и вычислив компонент вдоль w и h, векторы от центра до правой границы экрана и верхней границы экрана соответственно.
Благодаря процессу преобразования вершин в координаты экрана рисование граней становится упражнением по заполнению треугольников. Мы вычисляем три линейных уравнения, которые соединяют точки, затем шагаем по оси x, используя уравнения, чтобы определить минимальное и максимальное значения y для заданного значения x.
Мой главный вывод из этого проекта состоит в том, что линейная алгебра, которая используется при создании графического движка, не является недосягаемой для всех. В качестве курса средней школы или университета по линейной алгебре и Python или как способ самообучения конечный продукт чрезвычайно полезен, а работа вполне выполнима.
Конечно, есть много улучшений, которые можно и будут делать. Эта статья будет обновляться по мере внесения серьезных изменений (особенно повышения производительности). Две функции, которыми меня интересуют: отбраковка (выяснение, какие грани не нужно рисовать) и освещение или шейдеры (так что узлов может быть больше, чем только один сплошной цвет).
Каэден Уайл — студент Вашингтонского университета, соучредитель Offsite и основатель Kilometer Creative, где он выпустил несколько игр в App Store, включая TileForm и Landr.
В этой статье мы создадим небольшой игровой движок с видом от первого лица без сложной математики и техник 3D-визуализации, используя метод рейкастинга (трассировки, или «бросания», лучей).
Рейкастинг — один из методов рендеринга в компьютерной графике, при котором сцена строится на основе замеров пересечения лучей с визуализируемой поверхностью.
Игрок
Логично, что если мы создаем движок от первого лица, то наш игрок — это и есть точка, из которой будут выходить лучи. Для начала нам понадобится всего три свойства: координата x
, координата y
и направление:
function Player(x, y, direction) {
this.x = x;
this.y = y;
this.direction = direction;
}
Карта
Будем хранить карту с помощью двумерного массива. В нем 0 будет обозначать отсутствие стены, а 1 — её наличие. Для нашей реализации такой простой схемы будет достаточно.
function Map(size) {
this.size = size;
this.wallGrid = new Uint8Array(size * size);
}
Бросаем луч
Фишка в том, что при рейкастинге движок не рисует пространство целиком. Вместо этого он делит его на отдельные колонки и воспроизводит одну за одной. Каждая колонка представляет собой один брошенный под определенным углом луч. Если луч встречает на пути стену, он измеряет расстояние до нее и рисует прямоугольник в колонке. Высота прямоугольника определяется пройденным расстоянием — чем дальше стена, тем короче колонка.
Чем больше мы бросим лучей, тем более гладкими в результате будут переходы.
Найдем угол каждого луча
Угол зависит от трех параметров: направления, в котором смотрит игрок, фокусного расстояния камеры и колонки, которую мы в данный момент рисуем.
var x = column / this.resolution - 0.5;
var angle = Math.atan2(x, this.focalLength);
var ray = map.cast(player, player.direction + angle, this.range);
Проследим за каждым лучом на сетке
Нам нужно проверить наличие стен на пути каждого луча. В результате мы должны получить массив, в котором будут перечислены все стены, с которыми луч сталкивается, удаляясь от игрока.
Начинаем с того, что находим ближайшую к игроку горизонтальную stepX
и вертикальную stepY
линию сетки. Перемещаемся к той, что ближе, и проверяем на наличие стены с помощью inspect
. Повторяем шаги до тех пор, пока не отследим до конца траекторию каждого луча.
function ray(origin) {
var stepX = step(sin, cos, origin.x, origin.y);
var stepY = step(cos, sin, origin.y, origin.x, true);
var nextStep = stepX.length2 < stepY.length2
? inspect(stepX, 1, 0, origin.distance, stepX.y)
: inspect(stepY, 0, 1, origin.distance, stepY.x);
if (nextStep.distance > range) return [origin];
return [origin].concat(ray(nextStep));
}
Обнаружить пересечения на сетке легко: нужно просто найти все целочисленные x
(1, 2, 3…). А потом найти соответствующие y
с помощью умножения x
на коэффициент угла наклона rise / run
.
var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
var dy = dx * (rise / run);
Прелесть этой части алгоритма в том, что размер карты не имеет значения. Мы рассматриваем только определенный набор точек на сетке — каждый раз примерно одно и то же количество. В нашем примере размер карты 32×32, но если бы он был 32 000×32 000, скорость загрузки была бы такой же.
Рисуем колонку
После того как мы отследили луч, нам нужно нарисовать все стены, которые встречаются ему на пути.
var z = distance * Math.cos(angle);
var wallHeight = this.height * height / z;
Мы определяем высоту каждой стены, деля её максимальную высоту на z
. Чем дальше стена, тем короче мы её рисуем.
Откуда взялся косинус? Если мы будем использовать «чистое» расстояние от игрока до стены, то в итоге получим эффект «рыбьего глаза». Представьте, что вы стоите лицом к стене. Края стены слева и справа находятся от вас дальше, чем центр стены. Но мы же не хотим, чтобы при отрисовке стена выпирала посередине? Для того, чтобы визуализировать плоскую стену так, как мы её видим в реальной жизни, мы строим треугольник из каждого луча и находим перпендикуляр к стене с помощью косинуса. Вот так:
Слева — расстояние, справа — расстояние, умноженное на косинус угла.
В нашей статье это — самая сложная математика, с которой придется столкнуться 🙂
Визуализируем
Используем объект Camera, чтобы отрисовать карту с точки зрения игрока. Объект будет отвечать за визуализацию каждой колонки в процессе движения слева направо.
Прежде чем он отрисует стены, мы зададим skybox
— большое изображение для фона с горизонтом и звездами. После того, как закончим со стенами, добавим оружие на передний план.
Camera.prototype.render = function(player, map) {
this.drawSky(player.direction, map.skybox, map.light);
this.drawColumns(player, map);
this.drawWeapon(player.weapon, player.paces);
};
Самые важные свойства камеры — разрешение, фокусное расстояние и диапазон.
- Разрешение определяет, сколько колонок мы рисуем (сколько лучей бросаем);
- Фокусное расстояние определяет ширину линзы, через которую мы смотрим (углы лучей);
- Диапазон определяет дальность обзора (максимальная длина каждого луча).
Собираем воедино
Используем объект Controls
для снятия данных с клавиш-стрелок и сенсорной панели, а также объект GameLoop
для вызова requestAnimationFrame
. Цикл игры прописываем всего тремя строками:
loop.start(function frame(seconds) {
map.update(seconds);
player.update(controls.states, map, seconds);
camera.render(player, map);
});
Детали
Дождь
Дождь симулируем с помощью нескольких очень коротких стен, разбросанных произвольно:
var rainDrops = Math.pow(Math.random(), 3) * s;
var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance);
ctx.fillStyle = '#ffffff';
ctx.globalAlpha = 0.15;
while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height);
Задаем ширину стены в 1 пиксель.
Освещение и молнии
Освещение — это, вообще-то, работа с тенями: все стены рисуются со 100% яркостью, а потом покрываются черным прямоугольником какой-либо прозрачности. Прозрачность определяется как расстоянием до стены, так и её ориентацией (север / юг / запад / восток).
ctx.fillStyle = '#000000';
ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0);
ctx.fillRect(left, wall.top, width, wall.height);
Для симуляции молний, map.light
случайным образом совершает резкий скачок до значения 2 и потом так же быстро гаснет.
Предупреждение столкновений
Для того, чтобы игрок не натыкался на стены, мы просто проверяем его следующую локацию по карте. Координаты x
и y
проверяем по отдельности, чтобы игрок мог идти вдоль стены:
Player.prototype.walk = function(distance, map) {
var dx = Math.cos(this.direction) * distance;
var dy = Math.sin(this.direction) * distance;
if (map.get(this.x + dx, this.y) <= 0) this.x += dx;
if (map.get(this.x, this.y + dy) <= 0) this.y += dy;
};
Текстура стен
Без текстуры стена выглядела бы довольно скучно. Для каждой колонки мы определяем текстуру посредством взятия остатка в точке пересечения луча со стеной.
step.offset = offset - Math.floor(offset);
var textureX = Math.floor(texture.width * step.offset);
Например, для пересечения в точке (10, 8,2)
остаток равен 0,2. Это значит, что пересечение находится в 20% от левого края стены (8)
и 80% от правого края (9)
. Поэтому мы умножаем 0,2 на texture.width
чтобы найти x
-координату для изображения текстуры.
Можно посмотреть результат на сайте автора, а также изучить код на GitHub.
Перевод статьи «A first-person engine in 265 lines»