Как пишут код для игр

#Руководства

  • 11 окт 2018

  • 24

Как программировать игры: языки, движки и все, что нужно знать начинающему разработчику

Сперва это кажется дико сложным, но чем глубже погружаешься, тем лучше получается. Рассказываем, как начать делать игры,

 vlada_maestro / shutterstock

Евгений Кучерявый

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

Главное — в самом начале узнать, что нас ждёт, чтобы потом не свернуть на полпути, пройти все этапы и выпустить релиз. Подробно всем тонкостям, навыкам и хитростям мы обучаем на курсе «Профессия разработчик игр на Unity». Здесь же рассмотрим первые шаги, которые ждут разработчика.

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

  • Написать всё с нуля — сложный способ, но лучше начать с него, чтобы разобраться с языками.
  • Использовать движки — вариант полегче. Даже новичок, вооружившись мануалами из интернета, способен создать игру на одной из популярных платформ.
  • Найти заготовки — готовых шаблонов порой достаточно, чтобы выдать игру за свою, поменяв компоненты на свои.

Все три способа подразумевают какое-никакое программирование, так что знать хотя бы основы вам точно придётся.

Подойдут любые, от Python и C до Pascal и Java. От выбора зависит то, сколько времени уйдёт на игру и для какой платформы будет релиз. Также язык влияет на производительность.

На C++, например, пишут для любой платформы, а вот PHP или JavaScript лучше подходят для браузерных игр. Если же вы используете один из движков, то лучше вдобавок изучать C# — на нём прописывают скрипты. Главное — не недооценивать языки. Движок Unity дружит и с JavaScript, а MineCraft был написан на Java.

Среди современных выделим:

CryEngine

Crysis, Far Cry, Sniper II: Ghost Warrior.

Unreal Engine

Gears of War 4, Dead Pool, Mortal Kombat X, Tekken 7

Unity

Outlast, Assassin’s Creed: Identity, Temple Run, Deus Ex: The Fall.

Большой популярностью пользуется Unity, он рассчитан как на 2D-, так и на 3D-игры. Он подходит под разные платформы и языки. На нём создается большинство мобильных и инди-игр. Он бесплатный, но если вы зарабатываете на своих играх больше 100 тысяч долларов в год, то придётся делиться ими с разработчиками Unity.

Допустим, вы выбрали язык и движок, составили план. Что дальше? Продумайте всё от и до. В зависимости от выбранного вами пути (чистый язык или использование движка) будет отличаться и то, что вас ждёт на разных этапах разработки.

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

Физика — это то, как мир игры реагирует на действия игрока или объектов внутри мира. Вот какие могут быть физические действия:

  • ходьба;
  • езда;
  • прыжки;
  • удары;
  • выстрелы;
  • падение предметов и так далее.

Если пишете сами, то для обычного прыжка придется:

  • проверить, находится ли игрок на земле;
  • менять координату Y, чтобы игрок поднимался вверх;
  • закончить подъём через какое-то время;
  • начинать и продолжать падение до тех пор, пока под ногами игрока не окажется земля.

Не говоря уже о том, что нужно работать над анимацией всего этого.

function jump() {
If(gamer.jumpTimer >= 1) {
   gamer.y++; 
   gamer.jumpTimer --;
   } else {
      If(gamer.y >= ground.y) {
         gamer.y--;
      }
   }
}

В движках уже прописана физика, и нужно лишь подогнать её под свои нужды. Для примера:

  • Вы поместили объект в рабочую область Unity.
  • Указали, что для него нужно использовать физику.
  • При запуске игры объект упадёт.

И для этого не придётся писать код вообще — всё уже предусмотрено.

Игровая механика — это то, какими способами игрок взаимодействует с миром. Совокупность игровых механик составляет игровой процесс. Например, вы уже реализовали возможность ходьбы и прыжков. Эта игра, скорее, платформер.

А если добавите механику получения опыта, повышения уровней, прокачки навыков, — игра станет походить на RPG. Механика — такая же важная составляющая игры, как и сюжет или графика.

Ещё один пример: вы написали сценарий к игре, в которой нужно сбежать из тюрьмы. Даже если игра будет самой линейной в мире, игровая механика может всё изменить:

  • если добавить стрельбу, то будет экшн;
  • если игрок будет безоружен, — стелс;
  • если ещё и замки нужно взламывать, то это уже головоломка или пазл.

Будучи программистом, придётся уделять много времени механике.

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

В движке достаточно просто загрузить нужные файлы и прикрепить их к нужным моделям. Иначе — прописывать всё вручную, в том числе и анимацию.

Пример анимации двумерного персонажа

Для анимации 2D-объектов создаётся текстура по типу той, что на изображении выше. Она разбивается на равные части, которые сменяют друг друга. То есть игрок сначала видит первый кадр, который потом сменяется на второй, а затем на третий — это создает иллюзию движения.

Анимация в действии

Если брать 3D-модель, то используется скелетная анимация — модель как бы нанизывается на специальный каркас (скелет) с подвижными частями. Движение этих частей прописывается в коде.

На скриншоте видно, как персонаж сгибает руку в местах с точками (вершинами). Таких точек может быть очень много, если требуется сложная анимация — жесты, мимика и так далее.

Создаётся анимация так: прописываются точки координат или захватываются движения реального актера.

Первый способ сложный, но дешёвый, потому что от программиста требуется только прописать движения — сдвинуть точку A1 на координаты (50,240).

Второй проще, потому что достаточно одеть актеров в специальные костюмы с маячками, отснять это и перенести в игру. Но тут, конечно, придётся оплатить костюмы, павильон, работу операторов, постановщиков и актёров.

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

Например, если в шутере есть оружие с большим уроном, у него должна быть долгая перезарядка. Если урон маленький, перезарядка может быть быстрой. У бронированных врагов малая подвижность, а шустрые противники умирают с нескольких ударов или выстрелов.

Если геймплей предусматривает взаимодействие с NPC, то им нужно прописать модели поведения: реакцию на действия игрока, агрессивность, возможность вести диалоги или торговать.

Работа с ИИ — одна из самых сложных, потому что стоит учитывать множество ситуаций, для которых задумана реакция. Например, когда вы пытаетесь пройти в дверь, ваш компаньон обязательно должен преградить вам путь, чтобы жизнь малиной не казалась.

Разобравшись с тем, как всё будет устроено в игре, можно приступать к разработке. Но чтобы проект был коммерчески успешен, выбирайте популярные платформы. Всего можно выделить четыре:

  • Персональные компьютеры.
  • Приставки.
  • Мобильные устройства.
  • Браузер.

У каждой из этих платформ своя аудитория с вполне конкретными предпочтениями. На мобильных устройствах предпочитают головоломки (2048, 94%, Cut the Rope), аркады (Subway Surf, Temple Run, Angry Birds) и казуалы (Talking Cat Tom, Kitty Kate Baby Care, Hair Stylist Fashion Salon).

На компьютерах играют в MMORPG (Lineage II, World of Warcraft, Skyrim) или шутеры (Battlefield, Call of Duty, Counter-Strike).

Приставки подходят для гонок (Need for Speed, Blur, Burnout Paradise), приключенческих игр (Assassin’s Creed, Portal, The Walking Dead) и так далее.

В браузерах собирают пазлы и строят фермы.

Конечно, можно сделать и головоломку для PS4, и гонку для браузера — никто никого не ограничивает.

Будьте готовы к тому, что ваша первая игра не станет шедевром. Но не расстраивайтесь, потому что такие проекты отлично подходят для обучения.

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

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

Участвовать

Научитесь: Профессия Разработчик игр на Unity с нуля до Middle
Узнать больше

О чем статья?

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

Оглавление

  • Мотивация

    • Зачем так делать?

    • Почему C++?

    • Что нужно знать, чтобы понять статью?

  • Обработка действий игрока в реальном времени

    • Windows API

    • conio.h + getch

  • Запуск периодических событий по таймеру

    • Windows API

    • std::chrono

  • Генератор случайных чисел

  • Изменение размера экрана консоли

  • Ускорение вывода текста

    • Перестановка курсора

    • Вывод готовых фрагментов текста

    • Настройка буферизации stdio.h

    • Измерение производительности

Мотивация

Зачем так делать?

Чтобы научиться программировать. А точнее — чтобы научиться переносить человеческие мысли в код. Обычные задания по программированию плохо тренируют этот навык потому что они скучные. Признаюсь честно, я намеренно готовил скучные задания для своих студентов. Нудные задания на превращение двух чисел в третье неизбежны, чтобы освоить базовые концепции. Такие задания помогут вам спуститься по горной реке, заполненной острыми камнями синтаксиса и водоворотами новых концепций. Проблема в том, что простые понятные задания однажды бросят вас в бескрайнем океане возможностей. Тогда вы обнаружите, что можете плыть куда угодно. Однако никто не подскажет вам верное направление. Ни одна платформа с автоматической проверкой решений не сможет проверить творческую работу и поставить оценку. Это может сделать либо живой преподаватель, либо живой напарник.

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

Почему C++?

Главная причина в том, что я сейчас веду индивидуальные занятия по C++ у одного талантливого студента. Его успехи вдохновили меня, а его вопросы показали о чем вообще нужно написать. C++ до сих пор рекомендуют как «язык для обучения». Это вселяет надежду, что статья будет полезна многим. Может быть когда-нибудь я напишу такую же статью и для других языков или для Linux, но не рассчитывайте на это. Если вы напишете сами подобный сборник советов для другого языка, то сообщите мне личным сообщением. Я добавлю ссылку на ваш труд.

Что нужно знать, чтобы понять статью?

Если вы понимаете концепцию циклов, массивов и функций, то вам должно быть достаточно. Предупреждаю сразу, в статье будут магические конструкции, которые я не буду объяснять. У меня нет цели сделать всеобъемлющий курс по C++. Цель — писать код и радоваться тому, как оно почти магически заработает. Когда закончите с основной целью или когда встретитесь с непреодолимыми проблемами, тогда и углубляйтесь.

Обработка действий игрока в реальном времени

Проходя стандартную пачку учебных задач вы скорее всего использовали std::cin или scanf. Программа ждала от пользователя каких-то данных и нажатия кнопки Enter. Вся осмысленная работа происходила уже после того, как исходные данные получены и проверены. Но в настоящих играх специальную кнопку нужно нажимать только в пошаговых стратегиях. Остальные игры реагируют сразу при нажатии нужных кнопок. Поэтому главный прием для написания игры в реальном времени — моментальная обработка нажатия клавиш. Программа не должна ждать нажатия кнопки Enter. Игра должна работать и параллельно обрабатывать нажатие на клавиатуру. В профессиональном сообществе говорят, что когда нужно нажимать кнопку Enter, то это блокирующий ввод. Соответственно, когда программа работает и при этом готова обрабатывать команды — неблокирующий ввод.

Windows API

Когда я погуглил «non-blocking console input», то SO услужливо выдал мне вот такой вопрос https://stackoverflow.com/questions/6171132/non-blocking-console-input-c К сожалению, у него нет того ответа, который нужен нам. Вариант с фоновым потоком я считаю слишком сложным для того, чтобы кодить для своего удовольствия. Вариант с дополнительной нестандартной библиотекой — тоже. Вся остальная выдача не сильно отличалась по смыслу.

Существует ли такая возможность вообще? Наверняка да. Например в SDL.dll мы делаем графическое приложение и основной способ реагировать на клавиатуру с мышью — обрабатывать системные события. Вот есть целая глава руководства https://www.willusher.io/sdl2%20tutorials/2013/08/20/lesson-4-handling-events При этом тащить всю библиотеку SDL мне совсем не хотелось. Но раз нестандартная библиотека может обрабатывать события, значит любой сможет. Таким образом, чтобы программа работала и при этом моментально обрабатывала нажатия на клавиши, нужно отказаться от scanf и std::cin. Нам нужно пойти глубже — на уровень событий.

Поиск по ключевым словам «handle C++ event» выдало целую кучу бесполезной информации по рисованию окон в каком-то из фреймворков windows. Я все еще намерен писать простую игру в консоли, а не оконное приложение, поэтому игнорирую результаты.

Следующая попытка была «cpp event loop with console input» и мне попалась статья https://docs.microsoft.com/en-us/windows/console/reading-input-buffer-events из которой я и взял решение. Считаю это удачной находкой, потому что в процессе написания этой статьи пытался вспомнить свои запросы и даже слегка видоизмененный «event loop c++ with console application» уже не давал нужной информации. Пришлось смотреть историю поиска. Для будущих поколений я добавил сюда явный текст своих запросов, в надежде что будущие поиски будут более результативными.

Подстава в том, что даже эта статья на самом деле показывает пример блокирующего чтения «The function [ReadConsoleInput] does not return until at least one record is available to be read.» Поэтому я посмотрел на соседние статьи и нашел обзор низкоуровневых функций чтения https://docs.microsoft.com/en-us/windows/console/low-level-console-input-functions

В ней описана функция PeekConsoleInput, которая «If no records are available, the function returns immediately.», но которая при этом «Reads without removing the pending input records in an input buffer.». То есть если ее вызвать несколько раз подряд, то она получит информацию об одних и тех же событиях. К счастью там же еще описана функция FlushConsoleInputBuffer, которая удаляет все непрочитанные накопленные события. Сочетание этих двух функций позволит добиться нужного эффекта.

Я видоизменил код из найденной статьи https://docs.microsoft.com/en-us/windows/console/reading-input-buffer-events а именно:

  1. Удалил обработку событий мыши. Если понадобится, спишите ее сами из оригинала.

  2. Удалил колдовство над режимом консоли (функции GetConsoleMode/SetConsoleMode). Для обработки клавиатуры подходит и стандартный режим. Это позволило упростить обработку ошибок в функции ErrorExit.

  3. Добавил сквозной счетчик итераций внешнего цикла. Чтобы было видно работу программы при отсутствии событий.

Получилось вот так:

Обработка событий клавиатуры в консоли Windows API

Обработка событий клавиатуры в консоли Windows API

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

Результат запуска обработки событий клавиатуры в консоли Windows API

Результат запуска обработки событий клавиатуры в консоли Windows API

При запуске будет повторяться одна и та же фраза. Например «iteration 468811 total 106 current 2» где значение после iteration — счетчик, который увеличивается при каждой итерации цикла. Даже если мы ничего не будем нажимать. значение после total — количество событий, которые мы обработали значение после current — количество событий, которые нужно обработать в этой итерации цикла

Допустим мы увидели как обрабатывать события клавиатуры не останавливая работу программы. Что можно с этим сделать?

Чтобы собрать больше информации, я поставил точку останова в функции обработки события и понажимал разные кнопки.

Анализ результатов в отладчике

Например вот так выглядит событие при нажатии «ж».

Вот так — нажатие шифта

Вот так — нажатие «ж» с удерживаемым шифтом. Пришлось переставить точку остановки на «отпускание» клавиши, чтобы программа не реагировала на зажатый шифт. Обратите внимание, что dwControlKeyState отличается от «ж» без шифта.

Вот так — символ точка с запятой на английской раскладке. Это та же кнопка, что и «ж».

Вот так — символ точка с запятой на русской раскладке. Это уже другая кнопка — с цифрой «4».

Вот так — доллар. Он тоже на цифре «4», но на английской раскладке.

Обратите внимание, что во всех вариантах нажатия на кнопку «ж», «Ж», «;» в поле wVirtualKeyCode находится одно и то же число. Если использовать это поле, то управление не будет зависеть от раскладки и даже от зажатого шифта и капслока.

Еще важный момент, что wVirtualKeyCode для точки с запятой «;» на разных клавишах разный, но uChar.UnicodeChar у них одинаковый.

Итого с помощью Windows API мы можем:

  1. Различать нажатые клавиши по коду клавиши независимо от раскладки

  2. Различать нажатые клавиши по коду из Юникода независимо от их расположения на клавиатуре.

  3. Обрабатывать нажатия на shift, ctrl, alt.

  4. Понимать, нажат ли сейчас shift, ctrl, alt.

  5. Отличать нажатие клавиши и ее отпускание (буду благодарен, если подберете слово получше).

  6. Прикрутить обработку событий мыши

Если мы можем отличать нажатые кнопки, то можем и по-разному на них реагировать. Например менять координаты персонажа при нажатии на стрелки. Буду использовать обычную для программ координатную сетку. Ноль в ней находится слева сверху. Ось X увеличивается вправо, а ось Y — вниз. Для обозначения координат персонажа объявил две переменные: x и y с начальным значением 10.

В обработчик нажатия клавиш добавил ветвление. В нем проверяется код клавиши и увеличивается соответствующая переменная.

Для проверки запустил и нажал вниз 1 раз, вправо 2 раза, вверх 3 раза, влево 4 раза. Получился вывод как на картинке. Чтобы сделать скриншот, пришлось подключить отладчик.

conio.h + getch

Мой студент параллельно мне нашел способ обрабатывать нажатия на кнопки с помощью комбинации функций kbhit и getch из conio.h. С помощью getch можно получить код нажатого символа, но эта функция ждет следующего нажатия. Чтобы программа при этом продолжала работать, нужно сначала вызвать kbhit. Эта функция вернет true, если нажата хотя бы одна клавиша, но при следующего нажатия ждать не будет. Если ничего не нажато, то kbhit возвращает false и программа работает дальше.

К сожалению, я сходу не разобрался, в какой кодировке этот код для кириллических символов. Я реализовал прототип с помощью conio и поэкспериментировал с нажатием клавиш. Эта библиотека игнорирует нажатие shift, ctrl, alt, caps lock. Точка с запятой «;» на клавише с «ж» вернет код «59», на клавише с «4» тоже получился код 59. У символов в разных регистрах, например «ж» и «Ж» будут разные коды. У меня получились 166 и 134 соответственно. Интересно, что стандартное преобразование «(char)key» превратило эти коды в совершенно другие символы.

Обработка событий клавиатуры conio.h

Обработка событий клавиатуры conio.h
Результат запуска обработки событий с помощью conio.h
Результат запуска обработки событий с помощью conio.h

Получается, что conio проще чем winapi по написанию, но беднее по возможностям. Какой именно способ применить для вашей игры — выбирать только вам.

Запуск периодических событий по таймеру

Как вы можете заметить, если просто отпустить программу в свободный полет, то она будет очень часто обращаться к списку событий. На моем компьютере получается несколько десятков раз за каждую миллисекунду. Само по себе это хорошо, т.к. позволяет оперативно реагировать на все действия игрока. Однако если мир будет меняться с такой же скоростью, то игрок никак не сможет поспеть за ним. Есть несколько способов контролировать период обновления мира. Я выбрал вариант без подключения дополнительных библиотек. Основная идея в том, чтобы постоянно смотреть на время, а обновление мира запускать каждые несколько секунд. Раз уж я докопал до ручной реализации event-loop, то изобрести велосипед с реализацией задержки в рамках выбранной архитектуры не составит труда.

Конкретная реализация нагуглилась с первого запроса «windows.h time milliseconds». https://stackoverflow.com/questions/17008026/windows-how-to-get-the-current-time-in-milliseconds-in-c Для очистки совести я еще попробовал поискать «cpp thread sleep» и «cpp sleep». Первый вариант получился слишком сложный, а второй недостаточно точный.

Поэтому я предлагаю свой вариант. Цель — вписать ожидание в существующий цикл обработки событий. Для этого в начале программы я получаю текущее время. Если бы я работал с обычными часами, то это было бы 17:32:44. Затем я вычисляю время следующего события. Допустим оно должно произойти через 40 минут. Для этого к текущему времени прибавляю длительность ожидания. В моем примере событие произойдет в 18:12:44. Время второго события получается к времени первого события прибавляю длительность ожидания. 18:52:44 Врем третьего события 19:32:44 и так далее.

Существует несколько способов получить текущее время:

  1. С помощью GetSystemTime из windows.h

  2. С помощью std::chrono

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

Получение текущего времени Windows API

Функция GetSystemTime находится в библиотеке . Она записывает структуру с несколькими полями. Каждое поле отвечает за свою компоненту времени: год, месяц, день и так далее включая миллисекунды. Называются они довольно очевидно, так что я думаю вы догадаетесь или воспользуетесь переводчиком. В примере ниже структура со временем лежит в переменной tempTime. В отладчике она выглядит примерно так:

Обратите внимание, что все компоненты времени меняются от 0 до значения не превышающего следующую компоненту. Ну то есть часы от 0 до 23, минуты от 0 до 59. Миллисекунды от 0 до 999. Если сравнивать «в лоб», то возникнет проблема когда последнее обновление было в конце прошлой секунды, а текущее время — в текущей. У этого варианта есть два решения:

  1. Конвертировать полученную структуру в что-то вроде unix timestamp — одно число, начиная с «начала эпохи».

  2. Сделать хитрое сравнение.

Первый вариант может решить проблему только если я воспроизведу вычисление настоящего unix-timestamp, а мне лень. Например там будет сложность с количеством дней в месяце. Поэтому я сделаю менее точную версию, где «начало эпохи» будет в начале текущего месяца.

Код получается вот таким

Получение текущей даты Windows API

Получение текущей даты Windows API

При запуске получается примерно такой вывод:

Результат получения времени из Windows API

Результат получения времени из Windows API

Теперь добавляю дополнительную переменную для хранения времени предыдущего обновления (prevTime) и периода между обновлениями (delay).

Каждую итерацию цикла текущее время сравнивается со временем последнего обновления. Если разница больше задержки, то выполняется обновление. Дополнительно реализована функция для предупреждения перехода через ноль.

При запуске получается примерно такой вывод.

Таким образом команда вывода слова «tick» будет выполняться раз в 300 миллисекунд. После нее нужно помещать логику будущей игры.

Получение текущего времени std::chrono

Реализация с помощью std::chrono получилась значительно проще. Но в ней используется непривычное для обычных людей запись времени. Там нет количества часов с начала дня, минут с начала часа и секунд с начала минуты. С помощью std::chrono можно получить количество миллисекунд с «начала эпохи». То есть с 1 января 1970 года. Это получается огромное целое число. Например 1610827417491. В отличие от «количества миллисекунд с начала секунды», это число всегда увеличивается. Поэтому не возникнет необходимости беспокоиться о переполнении часов.

В этом варианте я также использовал другой способ сравнения времени. На одно арифметическое действие в цикле меньше. Мелочь, а приятно.

Получение текущего времени std::chrono

Получение текущего времени std::chrono

Константа PRIu64 нужна для форматированного вывода значения типа uint64_t с помощью printf. Для её использования нужно подключить inttypes.h Хотелось бы, конечно, не подключать лишних библиотек, но времени на обход именно этой библиотеки у меня не было.

Вывод получился вот таким

Вывод от получения текущего времени std::chrono

Вывод от получения текущего времени std::chrono

Что с этим можно сделать? Навскидку мне в голову пришло:

  1. Симуляция гравитации в платформере. Каждые Х миллисекунд персонаж должен падать на 1 символ.

  2. Полет пуль. Каждые Х миллисекунд передвинь пулю по направлению выстрела.

  3. Движение змейки. Каждые Х миллисекунд передвинь сегменты змейки на один символ от головы.

Что самое важное, у разных событий может быть разный период обновления. Вот прототип, где каждые 300 миллисекунд у персонажа уменьшается координата Х на 1, а каждые 225 миллисекунд — координата Y. При этом игрок может на WASD влиять на эти координаты гораздо чаще.

На картинке ниже вывод, который получается в такой программе время. Обратите внимание на чередование фраз «Each 225» и «Each 300».

Генератор случайных чисел

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

По запросу «с++ rand» и «cpp rand» можно найти довольно много материалов. Например обширную статью на русском https://en.cppreference.com/w/cpp/numeric/random/rand и чуть более сухую на английском https://en.cppreference.com/w/cpp/numeric/random/rand

Вкратце перескажу основные тезисы:

  1. Функция rand возвращает случайное число от 0 до RAND_MAX. Значение RAND_MAX зависит от библиотеки, но должно быть не менее 32767.

  2. Если просто вызывать функцию rand, то при разных запусках, последовательность случайных чисел будет одинаковой.

  3. Чтобы последовательность была каждый раз разной, нужно применить функцию srand.

Я от себя добавлю, что аргумент функции srand еще называют зерном (или «сидом» от слова seed). Вы подобное видели во многих играх при создании мира.

Примеры начальных значений для ГПСЧ

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

При запуске у меня появляется фраза «first is 440 second is 19053». Независимо от количества запусков получаются одни и те же числа. У вас могут получиться другие числа, но от запуска к запуску они должны быть одинаковы.

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

Ответ — время запуска. Простейший способ задания зерна случайности — взять текущее время. Обычно во всяких примерах предлагают подключить библиотеку ctime и вызывать функцию std::time(nullptr). Этот вариант не устраивает меня потому что библиотека ctime больше ни для чего не используется. Зато при реализации периодических событий была подключена библиотека chrono. В примере ниже зерно мира задается текущим временем.

Инициализация зерна случайности с помощью std::chrono

Инициализация зерна случайности с помощью std::chrono

Я варварски сконвертировал uion64_t в int, но этого оказалось достаточно.

Следующая проблема в том, что числа получаются огромные. Повторюсь, что rand возвращает число от 0 до RAND_MAX, который у меня равен 0x7fff. В десятичной системе счисления диапазон случайных чисел получается от 0 до 32767.

Классический способ уменьшить этот диапазон — применить операцию «остаток от деления». Выражение выглядит как «std::rand() % boundary», где boundary — новое ограничение диапазона.

Вы когда-нибудь задумывались, а почему остаток от деления нам действительно помогает? Не получится ли такого, что какое-то число будет встречаться чаще других?

Строгое математическое доказательство будет чрезмерным для этой статьи. Поэтому постараюсь объяснить «на пальцах».

Посмотрите на два столбца чисел. Левый столбец — просто числа по порядку от 0 до 14. Правый столбец — остаток от деления числа из левого столбца на 4.

0 0

1 1

2 2

3 3

4 0

5 1

6 2

7 3

8 0

9 1

10 2

11 3

12 0

13 1

14 2

Как вы можете заметить, диапазон от 0 до 14 разделился на несколько отрезков от 0 до 3. Числа в правом столбце возрастают так же равномерно, как и в левом. Точно такая же закономерность прослеживается и на диапазоне от 0 до 32767. Функция rand() может вернуть каждое число «из левого столбца» с равной вероятностью. После нахождения остатка от деления мы получим соответствующее число из правого столбца.

Обратите внимание, что последний диапазон «от 0 до 3» в правом столбце не успел закончиться. Если числа в левом диапазоне будут выбираться с равной вероятностью, то числа 0, 1, 2 из правого столбца будут появляться чаще, чем число 3. То же самое будет и на полном масштабе, если 32767 не будет делиться нацело на выбранное вами ограничение. Впрочем, исходный диапазон рандома достаточно велик, чтобы мы не заметили этот небольшой недостаток. В документации на cppreference в формуле выбора числа есть попытка компенсировать его делением на «((RAND_MAX + 1u)/6)».

Как уже говорилось выше, рандом возвращает числа от 0 до ограничения. Проблема в том, что при написании игры нужно использовать числа не от 0, а например от 10 до 20. В реализации std::rand приходится упражняться со сложением и вычитанием. К счастью, я нашел ссылку на статью с описанием синтаксиса, который был добавлен в стандарт C++ от 2011 года. https://en.cppreference.com/w/cpp/numeric/random/uniformintdistribution В нем есть красивый способ описать рандом в нужном диапазоне. Немножко оформления и можно просто получать случайные числа в нужном диапазоне.

#include <random>
#include <stdio.h>

int main()
{
	std::random_device rd;//Источник зерна для рандома
	std::mt19937 gen(rd());//Вихрь Мерсенна (Mersenne Twister) 
	std::uniform_int_distribution<> oneToSix(1, 6);//функция распределения от 1 до 6
	std::uniform_int_distribution<> twentyToForty(20, 40);//функция распределения от 20 до 40
  //Два числа в диапазоне от 1 до 6
  int first = oneToSix(gen);
  int second = oneToSix(gen);

  printf("%d %d ", first, second);
  
  //два числа в диапазоне от 20 до 40
  int third = twentyToForty(gen);
  int fourth = twentyToForty(gen);

  printf("%d %d", third, fourth);
 
  return 0;
}

Подробнее о Вихре Мерсенна можно почитать на Википедии https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D1%85%D1%80%D1%8C_%D0%9C%D0%B5%D1%80%D1%81%D0%B5%D0%BD%D0%BD%D0%B0

Меня такая находка очень порадовала. Пора переходить в туториалах для новичков на стандарт C++ хотя бы десятилетней давности. Этот фрагмент кода я специально для вас оформил текстом, который можно скопировать.

Изменение размера консоли

По умолчанию у меня в консоли помещается 80 символов в высоту и 50 в ширину. Это примерно треть моего экрана, поэтому захотелось увеличить количество символов в строке консоли хотя бы до 200.

Изменение размера окна консоли тоже оказалось задачкой с подвохом. Первый запрос был тривиальным «c++ change size of console window». Первый ответ на него подробно объяснял как сделать это с помощью настроек окна консоли на уровне операционной системы. То есть не из самой игры, а со стороны пользователя. Прикладывать эту инструкцию к игре я посчитал неправильным. Нужен способ сделать это из самой программы. Второй и последующие ответы описывали изменение размера окна консоли с помощью функции MoveWindow. Фактическое количество текста при этом не менялось. Если окно становилось слишком маленьким, то появлялись полосы прокрутки.

Следующая попытка была «c++ set console size». Два первых ответа вели на известные советы с функцией MoveWindow. Зато дальше пошли ссылки на документацию. А именно — на функцию SetConsoleScreenBufferSize. Судя по описанию, она меняет не размер видимого окна, а внутренний размер буфера. В качестве аргументов она принимает поток вывода и структуру с желаемыми размерами буфера.

На тот момент я не знал точно, какие размеры стандартные и что я могу туда поставить. Поэтому указал 20 на 20. Для проверки размеров окна я также вывел прямоугольник из цифр от 0 до 9 шириной 20 на 20. Получился вот такой код:

Заполнение экрана символами и изменение размера консоли

Заполнение экрана символами и изменение размера консоли

Вывод получился вот таким

Поскольку это работа на уровне WinAPI, в результате получился код ошибки. Я в основном работаю с java стеком и обычно вижу стектрейсы и тексты исключений. Несмотря на это, принцип решения проблемы не изменился. Для расшифровки кода ошибки нужно воспользоваться официальной документацией. Она легко ищется запросом «getlasterror error codes». Кодов ошибок описано около девяти тысяч на нескольких страницах. Для моего случая подойдет первая страница https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes—0-499-

Ошибка гласит ERROR-INVALID-PARAMETER 87 (0x57) The parameter is incorrect.

Маловато объяснений. Тогда я проверил как другие пишут этот код. Запрос «SetConsoleScreenBufferSize incorrect argument» привел меня вот на этот вопрос на SO https://stackoverflow.com/questions/12900713/reducing-console-size

В ключевых аспектах код ответа был похож на мой. Но в нем содержалось важное дополнение «If you call SetConsoleScreenBufferSize with illegal value in COORDS (e.g. too little height/width) then you get an error, usually 87 ‘invalid argument’.»

Потом я посмотрел в документацию к функции SetConsoleScreenBufferSize https://docs.microsoft.com/en-us/windows/console/setconsolescreenbuffersize и увидел что на размеры буфера наложены ограничения. Получается, что я передал слишком маленькие значения. У меня не было необходимости перебирать значения для получения точных минимальных размеров. В конце концов цель — увеличить размеры буфера, а не уменьшить. Поэтому показалось логичным отталкиваться от текущих размеров окна. Раз у нас есть функция SetЧтототам, значит должна быть и функция GetЧтототам. GetConsoleScreenBufferInfo действительно нашлась https://docs.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo С помощью неё и отладчика MSVS я выяснил, что размеры буфера на моей машине по умолчанию 80 на 50. Ширину я увеличил примерно в три раза, а высоту в полтора. При инициализации структуры size значением X = 200 и Y = 80 в высоту появились полосы прокрутки. Здесь и пригодилась функция MoveWindow.

Исходный код был видоизменен вот так:

Вывод при этом получился таким

Ускорение вывода текста

Запустив программу для заполнения большого экрана символами, я обнаружил, что текст пишется в консоль очень медленно. Заполнение символами экрана 200 на 80 заметно человеческому глазу. За одну секунду получится обновить экран лишь 1-2 раза. Это вряд ли связано с производительностью компьютера. Интуиция подсказывает, что это искусственное ограничение. При решении этой проблемы у меня было два направления поиска:

  1. Как быстро написать много текста?

  2. Как обновить только тот фрагмент экрана, который действительно менялся?

Сначала я поискал «cpp console reduce delay between screen updates». Практически все ссылки вели на советы по добавлению паузы, что мне совершенно не интересно. Только один ответ в выдаче говорил что-либо об ускорении вывода https://stackoverflow.com/questions/26376094/c-writing-to-console-without-delays. В нем предлагается подготовить большой буфер в памяти и вывести его одной командой.

Затем я поискал «windows.h write lot of text without animation» и нашел вот такой вопрос с очень любопытным ответом. https://stackoverflow.com/questions/34842526/update-console-without-flickering-c

Автор вопроса и автор ответа разговаривают в контексте создания консольной игры. Вместо полной очистки и полного заполнения экрана в ответе предлагается:

  1. Переставить курсор и писать новый текст поверх старого без предварительной очистки экрана.

  2. Переписывать фрагменты, а не отдельные буквы.

  3. Переписывать отдельные буквы на экране.

  4. Использовать два буфера и писать на экран только разницу.

Обратите внимание, в ответе на вопрос есть еще пример настройки цвета текста в консоли. У меня, к сожалению, не хватило времени воспроизвести этот прием.

Перестановка курсора

Простой перенос курсора в верхний левый угол экрана значительно улучшил ситуацию на windows 7. Я все еще видел процесс заполнения экрана, но текст на экране не исчезал два раза в секунду. У меня пропадали некоторые линии. У моего студента была windows 10 и без дополнительных ухищрений было видно только мигание самого курсора в разных частях экрана. Пропадания линий замечено не было.

За основу был взят код из главы «Изменение размера консоли».

Для своего удобства я реализовал функцию заполнения структуры нужными координатами.

Функция подготовки структуры с координатами

Функция подготовки структуры с координатами

Для перестановки курсора достаточно вызвать функцию SetConsoleCursorPosition и передать ей в качестве аргументов поток вывода и структуру с координатами желаемой позиции курсора.

Вызов функции SetConsoleCursorPosition для перестановки курсора

Вызов функции SetConsoleCursorPosition для перестановки курсора

Как я уже говорил, это улучшило ситуацию, но проблему полностью не решило. Поэтому я решил раскопать поглубже.

Вывод готовых фрагментов текста

Следующая попытка — писать символы не по одному, а строчками. Для этого нужно подготовить массив символов в памяти, который я далее буду писать как целую строку. Длина этого массива совпадает с шириной окна консоли. Во внутреннем цикле массив будет заполняться, а во внешнем — выводиться на экран. Ранее я выводил цифры с помощью форматированного вывода. Теперь нужно писать именно цифры. Для простой конвертации числа в символ нужно знать коды этих символов. У цифры «0» код символа 48, у цифры «9» — 57. То есть к числу от 0 до 9 включительно достаточно просто прибавить 48. Напомню, что последний символ в буфере обязательно должен быть «», чтобы не ловить баги, связанные с выводом нежелательных символов. За основу я взял код из раздела про изменение размера консоли.

Вывод текста фрагментами

Вывод текста фрагментами

В примерах еще часто использовался std::cout.flush(), который тащит за собой подключение iostream. Мне не хотелось использовать дополнительную библиотеку. Наверняка аналог есть и в stdio, который уже подключен. Мой запрос для поиска был «stdio flush output». Две ссылки на Stackoverflow указывают на fflush

  • https://stackoverflow.com/questions/12450066/flushing-buffers-in-c/12450125

  • https://stackoverflow.com/questions/1716296/why-does-printf-not-flush-after-the-call-unless-a-newline-is-in-the-format-strin

Функция fflush вызывается с аргументом stdout. Я минут 10 искал как правильно заполнить переменную stdout, а оказалось она просто доступна из глобальной области видимости.

Настройка буферизации stdio.h

Подозреваю, что в stdio.h буферизация вывода уже реализована, поэтому мой код скорее всего оказался велосипедом. На момент оформления этой статьи я уже забыл как искал информацию. Главное — для настройки буфера с помощью stdio.h нужно воспользоваться функцией setvbuf. Она принимает stdout, буфер, какие-то флаги и число — размер буфера.

Настройка буфера вывода с помощью stdio.h

Настройка буфера вывода с помощью stdio.h

Измерение производительности

Тут я осознал, что не в состоянии сравнить производительность разных вариантов «на глаз». Поэтому вкрутил измерение времени до и после обновления экрана. Напишу тут порядок цифр, потому что значения с точностью до микросекунд не существенны для сравнения. Код практически полностью списал из ответа к этому вопросу https://stackoverflow.com/a/21856299

Функция получения времени с точностью до микросекунд

Функция получения времени с точностью до микросекунд

Само измерение — тривиально. Получаю текущее время до и после отрисовки экрана. Затем вычитаю и вывожу на экран. Есть способы лучше, но для моей задачи этого оказалось достаточно. Код ниже — развитие варианта с настройкой буфера с помощью stdio.h Код варианта с велосипедным буфером принципиально не отличался, поэтому не публикую его.

Измерил количество микросекунд без явного указания буфера. Три запуска, три числа: 8930000 8880000 9220000.

с размером буфера 1/16 от 740000 до 750000 микросекунд

с размером буфера 1/8 от 40000 до 39000 микросекунд

с размером буфера 1/4 от 18000 до 19000 микросекунд

с размером буфера 1/2 от 12000 до 13000 микросекунд

с размером буфера равным ширине консоли от 90000 до 10000.

Во всех этих случаях наблюдается мигание строк на экране размером с буфер. То есть при буфере 1/4, мигают фрагменты в четверть строки. С буфером равным ширине консоли получается очень большой разброс, причем без промежуточных значений. либо за 10000, либо за 90000. При этом мигает так, как будто буфер половина. redraw настройка буфера библиотеки.PNG

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

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

Заключение

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

В качестве бонуса — ссылка на подробный разбор вывода русского текста в консоль. https://ru.stackoverflow.com/questions/459154/%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9-%D1%8F%D0%B7%D1%8B%D0%BA-%D0%B2-%D0%BA%D0%BE%D0%BD%D1%81%D0%BE%D0%BB%D0%B8

P.S. Если вы нашли опечатки или ошибки в тексте, пожалуйста, сообщите мне. Это можно сделать выделив часть текста и нажав «Ctrl / ⌘ + Enter», если у вас есть Ctrl / ⌘, либо через личные сообщения. Если же оба варианта недоступны, напишите об ошибках в комментариях. Спасибо!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Почему начали изучать программирование?


7.62%
Чтобы получать большую зарплату
8


37.14%
Чтобы разрабатывать игры
39


12.38%
Чтобы оживлять железки
13

Проголосовали 105 пользователей.

Воздержался 21 пользователь.

Что вы делаете, когда вы играете в какую-то игру и несколько раз не можете пройти одно и то же место? Можно просто найти уже готовое прохождение. Это, конечно, здорово, но что, если вы застряли в разработке игры? Какой гайд прочитать? Так пусть наша статья станет вашим пошаговым руководством к разработке вашей первой игры, если вы все еще не начали ее писать. Если уже начали, то вам все равно не помешает прочитать наши советы — быть может, увидите что-нибудь новое.

Прежде, чем мы начнем, мы хотим привести вам пример первой игры от автора этой статьи. Это была простая текстовая игра под названием Divine Blood:

01

Это была самая первая игра, причем запрограммированная для калькулятора TI-83 Plus. Позднее она была переписана автором на Java во время обучения в старших классах. Примечательно, что игра так и не была закончена.

Ко всеобщему сожалению, не доводить игры до логического конца — одна из самых распространенных проблем начинающих разработчиков. Соответственно, возникает вопрос: как повысить свою продуктивность и все-таки закончить начатый проект? Ответ прост: прочтите наше «прохождение» разработки игры.

Основные этапы

Если вы разрабатываете игру сами (а начинающие программисты обычно пишут как раз в одиночку), то процесс написания можно разделить на 4 основных этапа (или уровня, как и в играх):

  • планирование;
  • прототипирование;
  • программирование;
  • релиз.

Каждый следующий уровень в видеоигре сложнее, чем предыдущий. Здесь все также: каждый следующий этап имеет свои трудности и проблемы, с которыми вы неизбежно встретитесь. Это пошаговое руководство поможет вам не остановиться ни на одном этапе разработки игры. Мы приведем различные цитаты и мнения профессионалов, целиком прошедшие тот путь, который мы только начинаем.

В играх каждый уровень имеет свои проблемные места, а в конце — еще и босса. Так будет и у нас. Мы остановимся на них более подробно и, как и в любом прохождении, мы покажем, как пройти эти места и двинуться дальше.

Уровень 1: Планирование

Помните, сделав 90% игры, вы думаете, что потратили 90% своего времени. А доделывая оставшиеся 10%, вы тратите еще столько же «оставшегося» 90% времени. Составляйте план, учитывая это. — Ян Шрейбер

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

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

02

Как уже говорилось выше — уделяйте внимание деталям. Записали новую фичу для реализации? Отметьте рядом, как она будет работать, как будет влиять на игрока и непосредственно на игровой процесс. И помните, секрет успешной разработки — решать существующие проблемы, пока они не накопились.

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

Именно поэтому важно спланировать все, иначе на этапе разработки (уровень 3) вы захотите добавить в игру больше возможностей и начнете реализовывать все подряд, что категорически неправильно. Ваш проект рискует быть чрезмерно большим, отчего вам сложнее будет его контролировать, а вскоре вы вовсе бросите его в дальний ящик в надежде, что возьметесь за него позже. Если вы играли в серию игр Halo, представьте себе, как было бы трудно играть в нее, если бы вы столкнулись с парочкой охотников сразу после начала игры. Вы попросту будете умирать снова и снова, пока вам не надоест эта череда смертей. В разработке игры все аналогично.

Основная проблема первого этапа — притупить свое желание кодить и начать планировать. Написать код вы всегда сможете, для этого у вас будет целый этап. Спланируйте как можно больше аспектов вашей игры.

А боссом этого уровня являются вопросы. Просмотрите на все свои заметки и убедитесь в том, что у вас нет каких-либо непонятных пунктов: ни в используемых инструментах, ни в алгоритмах и прочем. Если же у вас действительно все вопросы решены, то вы смело можете считать, что уровень «Планирование» закончен. Но если остались непонятные аспекты — решите их, прежде чем переходить дальше.

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

Уровень 2: Прототипирование

Дизайн это процесс, а не результат. — Кристофер Симмонс

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

На этом этапе вы будете писать код не очень красивый и не совсем правильный. Это в порядке вещей, поскольку вы пишете прототип. Когда вы перейдете дальше, вы уже будете знать, что и где работает не так, как должно.

Если вы получаете удовольствие от какой-либо игры, то вы захотите поиграть в нее снова, независимо от того, сколько очков или денег вы там набрали. — Стэн Яроцки

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

Вы помните секретные телепорты в игре Super Mario Bros? Игрок мог найти хорошо спрятанные трубы, прыгнув в которые можно было пропустить несколько уровней. Так и здесь. Правда, мы не полностью пропускаем прототипирование, а совмещаем его с планированием.

03

Хотим заметить, что первые два этапа взаимозаменяемы. Быть может, вы хотите проверить основную механику вашей игры, прежде чем потратите кучу времени на детали? А может, вы хотите попробовать какую-то возможность в вашей игре? Это основные причины, почему есть смысл в том, чтобы поменять порядок первых двух этапов.

Хорошая игра — это поток интересных задач, решаемых игроком. — Брюс Шелли

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

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

Уровень 3: Программирование

Третий уровень — наиболее сложный уровень для начинающих программистов. Дойдя до него, многие забрасывают свой проект. Но бояться здесь ничего не надо! Первые два этапа пройдены и вы уже на полпути к окончанию разработки.

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

На самом деле эти два шага можно выполнить в обратном порядке. Имея какую-либо среду разработки (или язык программирования), вы можете проанализировать ее возможности и решить, что вы сможете написать.

Вы очень сильно облегчите себе жизнь, если воспользуетесь бесплатными библиотеками и ресурсами. Не пытайтесь изобретать велосипед: используйте то, что находится в свободном доступе. Это поможет вам приберечь немного сил для того, чтобы закончить проект.

«Я не умею рисовать» — используй клипарт. «Я не умею создавать звуки» — ищи MIDI файлы. «Я не умею программировать» — никого это не волнует, не программируй! — Роб

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

Но вы не должны унывать! Лучшее решение этих проблем — отвлечься от проекта на несколько дней. Вы очистите ваш ум и позволите новым идеям посетить его. Также неоднократно замечено, что «утро вечера мудренее». Застряли? Не знаете как решить проблему? Ложитесь спать, а на завтрашнее утро вы, возможно, сразу поймете причину ваших неудач. Не работайте до изнеможения и не изнуряйте себя: работа над проектом должна быть в удовольствие.

Если вы пытаетесь и не можете решить действительно сложную проблему — остановитесь. Решите более простую проблему, которая выполняет те же действия, что и сложная. — Даниель Кук

Ну а боссом этого уровня является сама игра. Да, игра, которую вы и пишете. Она не должна быть идеальной, но она должна быть полноценной. Такой, в которую бы люди могли и хотели играть.

Уровень 4: Релиз

Никогда не сдавайся. — Элис Тейлор

Наконец-то мы добрались и до релиза. Конечно же, этот уровень не такой сложный, как предыдущий, но и легкомысленно относится к нему не стоит. На этом этапе вы должны убедить людей играть в вашу игру и давать вам обратную связь (вы же хотите улучшить свой проект?). Основываясь на отзывах игроков, внесите в ваше приложение те изменения, которые сделают игру более увлекательной по вашему мнению.

Главная проблема этого этапа — критика. Всегда найдутся те, кому попросту не понравится ваша игра. Это нормально. Не стоить из-за таких людей опускать руки. Ориентируйтесь лучше на тех, кто играет в ваше приложение и предлагает вам добавить в нее новые возможности.

Начиная создавать игру, не смотрите на результаты других. Создайте то, что хотите вы. — Роб

А финальным боссом всего нашего путешествия будет являться ваша гордость. Вы сделали полноценную игру от начала и до конца! На самом деле, не все могут похвастаться этим.

Каждый разработчик когда-нибудь напишет «плохую» игру. Если вы еще не написали — напишете. Все мы напишем. — Бренда Ромеро

И помните, дорога к успеху вымощена многократными неудачами. Никогда не сдавайтесь!

Вывод

Разработка первой игры — захватывающее занятие, выполняя которое мы получаем бесценный опыт. И все же, многие разработчики не могут довести свой проект до конца, хотя так категорически нельзя поступать. Следуя нашим четырем «уровням» разработки игры, вы увеличите свои шансы довести ваш проект до логического завершения.

А теперь соберитесь и напишите свою игру!

Перевод статьи «Making Your First Game: A Walkthrough for Game Developers»

This article is for anyone who is interested in game programming. I will take you through the basics of game programming. Here we are specifically focusing on the classic DOS games. I cover and give examples for: Determining Initial requirements, Developing an Interface, analyzing different elements of the game design in the program that we’re going to make, and developing logic for Gameplay.

  • Download source code — 44.3 KB

Before we actually jump into game programming, we need to know something called event driven programming. Event driven programming refers to that style of programming wherein the user of the application is free to choose from several options rather than be confined to a predetermined sequence of interactions with the program. Game programming is one common example of event driven programming. A game is a closed, i.e., complete and self sufficient formal system that represents a subset of reality. A game is a perfect combination of actions-reactions or event-responses where every response is based on the most-recently occurred event.

Elements of Game Programming

In general, a computer game has five elements:

  • Graphics
  • Sound
  • Interface
  • Gameplay
  • Story

Graphics

Graphics consists of any images that are displayed and any effects that are performed on them. This includes 3D objects, textures, 2D tiles, 2D full screen shots, Full Motion Video (FMV) and anything else that the player will see.

Sound

Sound consists of any music or sound effects that are played during the game. This includes starting music, CD music, MIDI or MOD tracks, Foley effects (environment sounds), and sound effects.

Interface

Sound consists of any music or sound effects that are played during the game. This includes starting music, CD music, MIDI or MOD tracks, Foley effects (environment sounds), and sound effects.

Gameplay

It encompasses how fun the game is, how immense it is, and the length of playability.

Story

The game’s story includes any background before the game starts, all information the player gains during the game or when they win and any information they learn about character in the game. A story is an element of a game. The difference between a story and a game is that a story represents the facts in an immutable (i.e., fixed) sequence, while a game represents a branching tree of sequences and allows the player to create his own story by making choice at each branch point.

Though graphics plays an important role in game programming, in this article we’re not going to emphasize upon graphics and sound element of a game. We shall be concentrating at elementary game programming through text based interfaces.

Game Design Sequence

Since game design requires one to explore one’s artistic abilities, it cannot be formulated in a step by step process. However, there are certain technical steps that one needs to follow in one way or another.
These are:

  1. Determining Initial requirements.
  2. Develop Interface.
  3. Develop Interface.
  4. Develop Logic for Scoring points.

We will look at each of these in detail.

Interface is another very important aspect of game programming. The interface is the mode of communication between the computer and the player. Like any human language, it is the funnel through which the programmer must squeeze the avalanche of thoughts, ideas and feelings that he/she seeks to share with the fellow player. Interface will dictate what can or cannot be done. Interface is composed of input and output. While developing interface, the programmer should develop the static display screens and dynamic display screen. Static display is the screen which remains unaffected by the player’s actions i.e., the input by the player. The dynamic display, on the other hand, is the screen which is governed by the player’s actions i.e., the input by the player.

Examples of some static display screens are:

Game selection screens

What options are available to the player on the game startup? This describes what options are on the menu, how and where it appears on what screen, how the player gets there, and how he gets out.

Game start screen

What does the screen looks like at the beginning of the game, what are the startup parameters, where are the characters, etc? What messages, if any are on screen, and where? Intro music? Etc.

Since the dynamic screen vary as per the input given by the player, their descriptions are too many to be listed here. Some examples:

Message Screens

While developing interfaces, you also need to work on screens in response to legal actions of the player, by intimating that he/she is on the right track. Also, you need to work on the screens required to warn the player in case he/she commits an illegal move or action.

End of game message

These screens include messages and responses to questions like: What happens when the player loses? What happens when the player wins? What happens when the player get the high score? Where does the player go when the game is over? How does he start a new game?

This step involves developing a proper logic for gameplay. This requires the game-programmer to answer many questions in the form of program-code. These questions include: How is game played? What are the controls? What is the Game Goal? How is the player going to achieve the game goal? etc. In other words, we must say that since game represents an event-driven situation, the game-programmer i.e., you must specify or program everything that includes:

  1. Determining Initial requirements

    While writing a game program, after selecting the goal-of-game, one needs to determine its initial requirements. For instance, to write a game program for guessing a number, you need to decide about a way to generate the number, number of players involved, number of chances allowed to the player, a scoring methodology etc. Here we are not aiming at making you a professional game programmer, rather we are concentrating more at giving you an idea of writing simple or elementary game programs.

    General Description of Game: The general description of a game involves the general overview of the game, how it works, what happens on each level, etc. It describes all parts of the game from the player’s perspective:

    • What he’s supposed to know before he starts playing.
    • What he sees.
    • What he does.
    • His intended reaction to what he sees and does.
  2. Develop Interface
  3. Develop Logic of Gameplay
    • responses to the user/player’s action.
    • responses to system events.
    • rules of the game.
    • if it’s a two player game (if the computer is a player), then the computer’s moves and actions.
  4. Develop Logic for Keeping Scores

    Developing logic for the scoring purposes is a subset of developing logic for the game play. For this, you must first decide the scoring policy that you’re going to follow in your game. You’re going to decide the maximum number of chances allowed, the scoring mechanism, whether it is tied to time or not, etc. During this phase, the milestone events are worked out and accordingly scoring (positively or negatively) is carried out.

    Milestone Events in a Game

    Every once in a while, the player needs to be rewarded (or penalized) somehow for reaching that point in the game. Each of these places where something special happens is called a Milestone Event. There are a gauge to let the player know he’s on the right (or wrong) track, and will encourage (or discourage) him to keep going.

Now that we have discussed these different phases in game-development, let us not develop a simple tic-tac-toe game.

General Description of the game:

  1. It’s a two player game so the program takes the name of two players and assign O and X to the players.
  2. Players enter their move turn by turn, into the box they choose.
  3. Program needs to assure that no box is overwritten.
  4. If the player tries to enter his/her move into the box that’s already taken by the other player the the chance passes over to the other player.
  5. Program needs to run till a player wins, want to quit the game or until there are no moves left.
  6. After a player wins, program displays the message and will ask the player if he/she wants to play again.

Now let us analyze different elements of the game design in the program that we’re going to make.

It’s a two player game, so we need two variables to store their names and run a loop to ask for the player to enter their move turn by turn. So we need another variable to store the turn to see which player is to enter the move. Here are the variables:

char name[2][30]; int chance; 

We need a function to handle the navigation to the boxes when the player presses arrow keys and hit the Enter button to enter his move in the box. We need another variable to track the current box the player is on at the movement. An array to store the values entered by the player. So here are the variables:

int box; char a[3][3]; int navigate(char a[3][3], int box, int player, int key);

Here in this function, char a[3][3] is the array that holds the moves. box is the box the player was on, and key is the key pressed.

Another variable is required to count the number of turns. There are nine boxes in total however the number of turns maybe more than nine because if the player tries to enter his move into a box that’s already taken, then the chance passes over to the other player.

int turns; 

We need a function to put the move into the box chosen by the player and we need to make sure that we don’t overwrite the value in a box:

void putintobox(char a[3][3], char ch, int box);

Here a[3][3] is used to represent the boxes, ch is the character ‘O’ or ‘X’, and box is the box into which the value is to be entered. Now how would we know what character to put into the box? Well, this function is called by the navigate function mentioned above. So if the navigate function is called like this: box = navigate(a[3][3],3,0,ENTER);, then it means that player1(here player1-0, player2 is represented by 2) needs to enter into box 3. The putintobox function checks if the box is taken and enter the value in to the array that represents the boxes (a[3][3]), and calls another function showbox(char ch, int box) to show the character on screen in the specified box.

checkforwin checks if the player has won the game or not and boxesleft will check if all boxes are filled. We would need another variable to check if the player wants to quit the game so – int quit;.

In order to interact with the user, many messages are displayed. Also the player is told if he won the game or if it’s a draw. The program will also ask if the player wants to play again. So in our program, the messages would be:

The logic of this program is to run a while loop that runs till a player wins, or all the boxes are filled up but no one won the game or if the user wants to quit. Now while the loop is running, the variable chance that tracks whose chance is it to enter the move is updated. A function will check what was the key what pressed by the user (user can enter only up, down, left, right or enter key) and moves the cursor to the specified box and enter the character assigned to the player into the array and displays that on the screen. It also makes sure that no box is overwritten. It the user tries to overwrite the box, chance is passed on to the other player as a penalty to the player who entered the wrong move. At the end of the program, the user is asked if he/she wants to play the game again.

  1. Initial requirements
  2. Developing Interface
    • Ask the name of the players.
    • Display whose chance is it to enter the move.
    • Display if a player wins or if it’s a draw.
    • Display a message when the player wants to quit.
    • Display a message asking if the player wants to play again.
  3. Developing logic for Gameplay

Here is the list of functions we would need:

  • void showframe(int posx, int posy) — This function will show the frame of the Tic Tac Toe on the specified position.
  • void showbox(int ch, int box) — This function will show a specified character into the specified box.
  • void putintobox(char a[3][3], char ch, int box) — This function is used to write the character into the array and will call showbox(ch,box).
  • void gotobox(int box) — This function will take the cursor into the specified box.
  • int navigate(char a[3][3], int box, int player, int key) — This function will be used to track the box number the user is at while he is pressing the arrows keys on the keyboard. This will also get the box number in which the user wants to enter the character assigned to him/her.
  • int checkforwin(char a[3][3]) — Checks if the player wins.
  • int boxesleft(char a[3][3]) — To check how many boxes are left.

Details of the function: void showframe(int posx, int posy)

void showframe(int posx, int posy)
{

  int hr=196, vr=179;   int crossbr=197;      int x=posx, y=posy;
  int i,j;

  gotoxy(35,4); cprintf("TIC TAC TOE");
  gotoxy(35,5); for(i=0;i<11;i++) cprintf("%c",223);


  for(i=0;i<2;i++)
  {
    for(j=1;j<=11;j++)
     {
      gotoxy(x,y);
      printf("%c",hr);
      x++;p;    x++;
     }
    x=posx; y+=2;
  }
  x=posx+3; y=posy-1;

  for(i=0;i<2;i++)
  {

   for(j=1;j<=5;j++)
   {
      gotoxy(x,y);
      printf("%c",vr);
      y++;
   }
   x+=4;y=posy-1;
  }


  x=posx+3; y=posy;  
  gotoxy(x,y);
  printf("%c",crossbr);

   x=posx+7; y=posy;      
  gotoxy(x,y);
  printf("%c",crossbr);

  x=posx+3; y=posy+2;                    
  gotoxy(x,y);
  printf("%c",crossbr);

  x=posx+7; y=posy+2;         
  gotoxy(x,y);
  printf("%c",crossbr);

}

void showbox(char ch, int box)

void showbox(char ch, int box)
{
   switch(box)
   {
     case 1 :	gotoxy(_x+1,_y-1); printf("%c",ch); break;      case 2 :	gotoxy(_x+5,_y-1); printf("%c",ch); break;      case 3 :	gotoxy(_x+9,_y-1); printf("%c",ch); break;      case 4 :	gotoxy(_x+1,_y+1); printf("%c",ch); break;      case 5 :	gotoxy(_x+5,_y+1); printf("%c",ch); break;      case 6 :	gotoxy(_x+9,_y+1); printf("%c",ch); break;      case 7 :	gotoxy(_x+1,_y+3); printf("%c",ch); break;      case 8 :	gotoxy(_x+5,_y+3); printf("%c",ch); break;      case 9 :	gotoxy(_x+9,_y+3); printf("%c",ch); break;    }
}

void putintobox(char a[3][3], char ch, int box)

void putintobox(char arr[3][3], char ch, int box)
{
  switch(box)
  {

    case 1 :	if(arr[0][0] != 'X' && arr[0][0]!= 'O')
                { arr[0][0] = ch;
		          showbox(ch,1);
		        }
		        break;

    case 2 :	if(arr[0][1] != 'X' && arr[0][1]!= 'O')
		        { arr[0][1] = ch;
		          showbox(ch,2);
		        }
		        break;

    case 3 :	if(arr[0][2] != 'X' && arr[0][2]!= 'O')
                { arr[0][2] = ch;
		          showbox(ch,3);
		        }
		        break;

    case 4 :	if(arr[1][0] != 'X' && arr[1][0]!= 'O')
		       { arr[1][0] = ch;
		         showbox(ch,4);
		       }
		        break;

    case 5 :	if(arr[1][1] != 'X' && arr[1][1]!= 'O')
		        { arr[1][1] = ch;
		         showbox(ch,5);
		        }
		        break;

    case 6 :	if(arr[1][2] != 'X' && arr[1][2]!= 'O')
		        { arr[1][2] = ch;
                  showbox(ch,6);
		        }
	        	break;

    case 7 :	if(arr[2][0] != 'X' && arr[2][0]!= 'O')
     		    { arr[2][0] = ch;
		          showbox(ch,7);
		        }
		        break;

    case 8 :	if(arr[2][1] != 'X' && arr[2][1]!= 'O')
		        { arr[2][1] = ch;
		          showbox(ch,8);
		        }
		        break;

    case 9 :	if(arr[2][2] != 'X' && arr[2][2]!= 'O')
		        { arr[2][2] = ch;
		          showbox(ch,9);
		        }
		        break;
   }}

void gotobox(int box)

void gotobox(int box)
{
  switch(box)
  {
    case 1 : gotoxy(_x+1,_y-1); break;
    case 2 : gotoxy(_x+5,_y-1); break;
    case 3 : gotoxy(_x+9,_y-1); break;
    case 4 : gotoxy(_x+1,_y+1); break;
    case 5 : gotoxy(_x+5,_y+1); break;     case 6 : gotoxy(_x+9,_y+1); break;     case 7 : gotoxy(_x+1,_y+3); break;     case 8 : gotoxy(_x+5,_y+3); break;     case 9 : gotoxy(_x+9,_y+3); break;

  }
}

int navigate(char a[3][3], int box, int player, int key)

int navigate(char arr[3][3], int box, int player, int key)
{
   switch(key)
   {
     case UPARROW    : if( (box!=1) || (box!=2) || (box!=3) )
		               { box-=3; if(box<1) box = 1; gotobox(box); }
	                   break;

     case DOWNARROW  : if( (box!=7) || (box!=8) || (box!=9) )
		               { box+=3; if(box>9) box = 9; gotobox(box); }
		               break;

     case LEFTARROW  : if( (box!=1) || (box!=4) || (box!=7) )
		               { box--;  if(box<1) box = 1; gotobox(box); }
		               break;

     case RIGHTARROW : if( (box!=3) || (box!=6) || (box!=9) )
		               { box++; if(box>9) box = 9; gotobox(box); }
		               break;

     case ENTER      : if(player == 0)
			            putintobox(arr,'O',box);
		               else if(player == 1)
			            putintobox(arr,'X',box);
		               break;
    }
 return box;
}

int checkforwin(char a[3][3])

int checkforwin(char arr[3][3])
{
  int w=0;


    if((arr[0][0] == arr[0][1]) && (arr[0][1] == arr[0][2])) w = 1;
  else if((arr[1][0] == arr[1][1]) && (arr[1][1] == arr[1][2])) w = 1;
  else if((arr[2][0] == arr[2][1]) && (arr[2][1] == arr[2][2])) w = 1;

    else if((arr[0][0] == arr[1][0]) && (arr[1][0] == arr[2][0])) w = 1;
  else if((arr[0][1] == arr[1][1]) && (arr[1][1] == arr[2][1])) w = 1;
  else if((arr[0][2] == arr[1][2]) && (arr[1][2] == arr[2][2])) w = 1;

    else if((arr[0][0] == arr[1][1]) && (arr[1][1] == arr[2][2])) w = 1;
  else if((arr[0][2] == arr[1][1]) && (arr[1][1] == arr[2][0])) w = 1;

  return w;
}

int boxesleft(char a[3][3])

int boxesleft(char a[3][3])
{
   int i,j,boxesleft=9;

   for(i=0;i<3;i++)
    { for(j=0;j<3;j++)
      { if((a[i][j] == 'X') ||(a[i][j] == 'O'))
          boxesleft--;
      }
    }

   return boxesleft;
}

Now we have all the functions in place, we move on to the third step that talks about Presentation.

We have actually taken care of presentation in some of the above written functions, haven’t we?

The ASCII character used in this program to display the vertical line is 179 and for horizontal line is 196. For a cross — 197. Check this website for more information about extended ASCII characters http://www.asciitable.com/.

You can even printout your ASCII table with a C program. Here is an example:

#include <stdio.h>

int main()
{
    FILE *fh;
    int  ch;
     
    fh = fopen("ascii.txt","r");
 
    for(i=0;i<256;i++)
     fprint(fh,"n%d - %c",i,i);
 
    fclose(fh);
    return 0;
}

Now we will talk about Exception Handling. Here in this program, we have kept it simple so nothing much to worry about here, however, we do take the name from the user. The array we use to store the name is of size 30. What if the user enters a string that is more than 30 in length? This would result in a buffer overflow. This may crash your program right away or produce unexpected results. To avoid this either we can write a function that would get a string from the stdin one by one and stops if Enter is pressed or if string is more than 30 OR we can use the inbuilt function known as fgets.

Finally, putting it all together:

#include <stdio.h>
#include <conio.h>

int main()
{
    
 
    showframe(12,25);
    printf("nPlayer 1, enter your name:"); fgets(name[0], 30, stdin);
    printf("nPlayer 2, enter your name:"); fgets(name[1], 30, stdin);
 
    printf("n%s, you take 0",name[0]);
    printf("n%s, you take X",name[1]); getch();
 
    clrscr();
 
    do
    {
       while(!enter)
       {
         if(khbit())
          ch = getch();
        
           switch(ch)
           {
             case UPARROW : box = navigate(a[3][3], box, player, UPARROW);
             .
             .
             .
           }
       }
       if(quit) break;
              win = checkforwin(a);
    
     }while(!win)
 
    if(win)
    { .
      .
    }
 
    else if(quit)
    {    .
         .
    }
 
 return 0;
}

View the complete source code and executable to take a look at how the program works.

Here are some screenshots of the working executable for Tic Tac Toe:

Image 1

Image 2

Image 3

This article was not a complete fully fledged article for game programming but I hope that you gain something out of it. If you have any questions, please feel free to email me at shine_hack@yahoo.com

Source code for this program can be obtained at: http://techstuff.x10.mx/articles/.

Happy programming!

Shine Jacob (Enot): shine_hack@yahoo.com

I love programming, learn about new technologies, algorithms, and problems solving using programming. I started off programming with C/C++. Though I learned PHP and web development but still stuck on C. Don’t know why but I kinda love it.

This article is for anyone who is interested in game programming. I will take you through the basics of game programming. Here we are specifically focusing on the classic DOS games. I cover and give examples for: Determining Initial requirements, Developing an Interface, analyzing different elements of the game design in the program that we’re going to make, and developing logic for Gameplay.

  • Download source code — 44.3 KB

Before we actually jump into game programming, we need to know something called event driven programming. Event driven programming refers to that style of programming wherein the user of the application is free to choose from several options rather than be confined to a predetermined sequence of interactions with the program. Game programming is one common example of event driven programming. A game is a closed, i.e., complete and self sufficient formal system that represents a subset of reality. A game is a perfect combination of actions-reactions or event-responses where every response is based on the most-recently occurred event.

Elements of Game Programming

In general, a computer game has five elements:

  • Graphics
  • Sound
  • Interface
  • Gameplay
  • Story

Graphics

Graphics consists of any images that are displayed and any effects that are performed on them. This includes 3D objects, textures, 2D tiles, 2D full screen shots, Full Motion Video (FMV) and anything else that the player will see.

Sound

Sound consists of any music or sound effects that are played during the game. This includes starting music, CD music, MIDI or MOD tracks, Foley effects (environment sounds), and sound effects.

Interface

Sound consists of any music or sound effects that are played during the game. This includes starting music, CD music, MIDI or MOD tracks, Foley effects (environment sounds), and sound effects.

Gameplay

It encompasses how fun the game is, how immense it is, and the length of playability.

Story

The game’s story includes any background before the game starts, all information the player gains during the game or when they win and any information they learn about character in the game. A story is an element of a game. The difference between a story and a game is that a story represents the facts in an immutable (i.e., fixed) sequence, while a game represents a branching tree of sequences and allows the player to create his own story by making choice at each branch point.

Though graphics plays an important role in game programming, in this article we’re not going to emphasize upon graphics and sound element of a game. We shall be concentrating at elementary game programming through text based interfaces.

Game Design Sequence

Since game design requires one to explore one’s artistic abilities, it cannot be formulated in a step by step process. However, there are certain technical steps that one needs to follow in one way or another.
These are:

  1. Determining Initial requirements.
  2. Develop Interface.
  3. Develop Interface.
  4. Develop Logic for Scoring points.

We will look at each of these in detail.

Interface is another very important aspect of game programming. The interface is the mode of communication between the computer and the player. Like any human language, it is the funnel through which the programmer must squeeze the avalanche of thoughts, ideas and feelings that he/she seeks to share with the fellow player. Interface will dictate what can or cannot be done. Interface is composed of input and output. While developing interface, the programmer should develop the static display screens and dynamic display screen. Static display is the screen which remains unaffected by the player’s actions i.e., the input by the player. The dynamic display, on the other hand, is the screen which is governed by the player’s actions i.e., the input by the player.

Examples of some static display screens are:

Game selection screens

What options are available to the player on the game startup? This describes what options are on the menu, how and where it appears on what screen, how the player gets there, and how he gets out.

Game start screen

What does the screen looks like at the beginning of the game, what are the startup parameters, where are the characters, etc? What messages, if any are on screen, and where? Intro music? Etc.

Since the dynamic screen vary as per the input given by the player, their descriptions are too many to be listed here. Some examples:

Message Screens

While developing interfaces, you also need to work on screens in response to legal actions of the player, by intimating that he/she is on the right track. Also, you need to work on the screens required to warn the player in case he/she commits an illegal move or action.

End of game message

These screens include messages and responses to questions like: What happens when the player loses? What happens when the player wins? What happens when the player get the high score? Where does the player go when the game is over? How does he start a new game?

This step involves developing a proper logic for gameplay. This requires the game-programmer to answer many questions in the form of program-code. These questions include: How is game played? What are the controls? What is the Game Goal? How is the player going to achieve the game goal? etc. In other words, we must say that since game represents an event-driven situation, the game-programmer i.e., you must specify or program everything that includes:

  1. Determining Initial requirements

    While writing a game program, after selecting the goal-of-game, one needs to determine its initial requirements. For instance, to write a game program for guessing a number, you need to decide about a way to generate the number, number of players involved, number of chances allowed to the player, a scoring methodology etc. Here we are not aiming at making you a professional game programmer, rather we are concentrating more at giving you an idea of writing simple or elementary game programs.

    General Description of Game: The general description of a game involves the general overview of the game, how it works, what happens on each level, etc. It describes all parts of the game from the player’s perspective:

    • What he’s supposed to know before he starts playing.
    • What he sees.
    • What he does.
    • His intended reaction to what he sees and does.
  2. Develop Interface
  3. Develop Logic of Gameplay
    • responses to the user/player’s action.
    • responses to system events.
    • rules of the game.
    • if it’s a two player game (if the computer is a player), then the computer’s moves and actions.
  4. Develop Logic for Keeping Scores

    Developing logic for the scoring purposes is a subset of developing logic for the game play. For this, you must first decide the scoring policy that you’re going to follow in your game. You’re going to decide the maximum number of chances allowed, the scoring mechanism, whether it is tied to time or not, etc. During this phase, the milestone events are worked out and accordingly scoring (positively or negatively) is carried out.

    Milestone Events in a Game

    Every once in a while, the player needs to be rewarded (or penalized) somehow for reaching that point in the game. Each of these places where something special happens is called a Milestone Event. There are a gauge to let the player know he’s on the right (or wrong) track, and will encourage (or discourage) him to keep going.

Now that we have discussed these different phases in game-development, let us not develop a simple tic-tac-toe game.

General Description of the game:

  1. It’s a two player game so the program takes the name of two players and assign O and X to the players.
  2. Players enter their move turn by turn, into the box they choose.
  3. Program needs to assure that no box is overwritten.
  4. If the player tries to enter his/her move into the box that’s already taken by the other player the the chance passes over to the other player.
  5. Program needs to run till a player wins, want to quit the game or until there are no moves left.
  6. After a player wins, program displays the message and will ask the player if he/she wants to play again.

Now let us analyze different elements of the game design in the program that we’re going to make.

It’s a two player game, so we need two variables to store their names and run a loop to ask for the player to enter their move turn by turn. So we need another variable to store the turn to see which player is to enter the move. Here are the variables:

char name[2][30]; int chance; 

We need a function to handle the navigation to the boxes when the player presses arrow keys and hit the Enter button to enter his move in the box. We need another variable to track the current box the player is on at the movement. An array to store the values entered by the player. So here are the variables:

int box; char a[3][3]; int navigate(char a[3][3], int box, int player, int key);

Here in this function, char a[3][3] is the array that holds the moves. box is the box the player was on, and key is the key pressed.

Another variable is required to count the number of turns. There are nine boxes in total however the number of turns maybe more than nine because if the player tries to enter his move into a box that’s already taken, then the chance passes over to the other player.

int turns; 

We need a function to put the move into the box chosen by the player and we need to make sure that we don’t overwrite the value in a box:

void putintobox(char a[3][3], char ch, int box);

Here a[3][3] is used to represent the boxes, ch is the character ‘O’ or ‘X’, and box is the box into which the value is to be entered. Now how would we know what character to put into the box? Well, this function is called by the navigate function mentioned above. So if the navigate function is called like this: box = navigate(a[3][3],3,0,ENTER);, then it means that player1(here player1-0, player2 is represented by 2) needs to enter into box 3. The putintobox function checks if the box is taken and enter the value in to the array that represents the boxes (a[3][3]), and calls another function showbox(char ch, int box) to show the character on screen in the specified box.

checkforwin checks if the player has won the game or not and boxesleft will check if all boxes are filled. We would need another variable to check if the player wants to quit the game so – int quit;.

In order to interact with the user, many messages are displayed. Also the player is told if he won the game or if it’s a draw. The program will also ask if the player wants to play again. So in our program, the messages would be:

The logic of this program is to run a while loop that runs till a player wins, or all the boxes are filled up but no one won the game or if the user wants to quit. Now while the loop is running, the variable chance that tracks whose chance is it to enter the move is updated. A function will check what was the key what pressed by the user (user can enter only up, down, left, right or enter key) and moves the cursor to the specified box and enter the character assigned to the player into the array and displays that on the screen. It also makes sure that no box is overwritten. It the user tries to overwrite the box, chance is passed on to the other player as a penalty to the player who entered the wrong move. At the end of the program, the user is asked if he/she wants to play the game again.

  1. Initial requirements
  2. Developing Interface
    • Ask the name of the players.
    • Display whose chance is it to enter the move.
    • Display if a player wins or if it’s a draw.
    • Display a message when the player wants to quit.
    • Display a message asking if the player wants to play again.
  3. Developing logic for Gameplay

Here is the list of functions we would need:

  • void showframe(int posx, int posy) — This function will show the frame of the Tic Tac Toe on the specified position.
  • void showbox(int ch, int box) — This function will show a specified character into the specified box.
  • void putintobox(char a[3][3], char ch, int box) — This function is used to write the character into the array and will call showbox(ch,box).
  • void gotobox(int box) — This function will take the cursor into the specified box.
  • int navigate(char a[3][3], int box, int player, int key) — This function will be used to track the box number the user is at while he is pressing the arrows keys on the keyboard. This will also get the box number in which the user wants to enter the character assigned to him/her.
  • int checkforwin(char a[3][3]) — Checks if the player wins.
  • int boxesleft(char a[3][3]) — To check how many boxes are left.

Details of the function: void showframe(int posx, int posy)

void showframe(int posx, int posy)
{

  int hr=196, vr=179;   int crossbr=197;      int x=posx, y=posy;
  int i,j;

  gotoxy(35,4); cprintf("TIC TAC TOE");
  gotoxy(35,5); for(i=0;i<11;i++) cprintf("%c",223);


  for(i=0;i<2;i++)
  {
    for(j=1;j<=11;j++)
     {
      gotoxy(x,y);
      printf("%c",hr);
      x++;p;    x++;
     }
    x=posx; y+=2;
  }
  x=posx+3; y=posy-1;

  for(i=0;i<2;i++)
  {

   for(j=1;j<=5;j++)
   {
      gotoxy(x,y);
      printf("%c",vr);
      y++;
   }
   x+=4;y=posy-1;
  }


  x=posx+3; y=posy;  
  gotoxy(x,y);
  printf("%c",crossbr);

   x=posx+7; y=posy;      
  gotoxy(x,y);
  printf("%c",crossbr);

  x=posx+3; y=posy+2;                    
  gotoxy(x,y);
  printf("%c",crossbr);

  x=posx+7; y=posy+2;         
  gotoxy(x,y);
  printf("%c",crossbr);

}

void showbox(char ch, int box)

void showbox(char ch, int box)
{
   switch(box)
   {
     case 1 :	gotoxy(_x+1,_y-1); printf("%c",ch); break;      case 2 :	gotoxy(_x+5,_y-1); printf("%c",ch); break;      case 3 :	gotoxy(_x+9,_y-1); printf("%c",ch); break;      case 4 :	gotoxy(_x+1,_y+1); printf("%c",ch); break;      case 5 :	gotoxy(_x+5,_y+1); printf("%c",ch); break;      case 6 :	gotoxy(_x+9,_y+1); printf("%c",ch); break;      case 7 :	gotoxy(_x+1,_y+3); printf("%c",ch); break;      case 8 :	gotoxy(_x+5,_y+3); printf("%c",ch); break;      case 9 :	gotoxy(_x+9,_y+3); printf("%c",ch); break;    }
}

void putintobox(char a[3][3], char ch, int box)

void putintobox(char arr[3][3], char ch, int box)
{
  switch(box)
  {

    case 1 :	if(arr[0][0] != 'X' && arr[0][0]!= 'O')
                { arr[0][0] = ch;
		          showbox(ch,1);
		        }
		        break;

    case 2 :	if(arr[0][1] != 'X' && arr[0][1]!= 'O')
		        { arr[0][1] = ch;
		          showbox(ch,2);
		        }
		        break;

    case 3 :	if(arr[0][2] != 'X' && arr[0][2]!= 'O')
                { arr[0][2] = ch;
		          showbox(ch,3);
		        }
		        break;

    case 4 :	if(arr[1][0] != 'X' && arr[1][0]!= 'O')
		       { arr[1][0] = ch;
		         showbox(ch,4);
		       }
		        break;

    case 5 :	if(arr[1][1] != 'X' && arr[1][1]!= 'O')
		        { arr[1][1] = ch;
		         showbox(ch,5);
		        }
		        break;

    case 6 :	if(arr[1][2] != 'X' && arr[1][2]!= 'O')
		        { arr[1][2] = ch;
                  showbox(ch,6);
		        }
	        	break;

    case 7 :	if(arr[2][0] != 'X' && arr[2][0]!= 'O')
     		    { arr[2][0] = ch;
		          showbox(ch,7);
		        }
		        break;

    case 8 :	if(arr[2][1] != 'X' && arr[2][1]!= 'O')
		        { arr[2][1] = ch;
		          showbox(ch,8);
		        }
		        break;

    case 9 :	if(arr[2][2] != 'X' && arr[2][2]!= 'O')
		        { arr[2][2] = ch;
		          showbox(ch,9);
		        }
		        break;
   }}

void gotobox(int box)

void gotobox(int box)
{
  switch(box)
  {
    case 1 : gotoxy(_x+1,_y-1); break;
    case 2 : gotoxy(_x+5,_y-1); break;
    case 3 : gotoxy(_x+9,_y-1); break;
    case 4 : gotoxy(_x+1,_y+1); break;
    case 5 : gotoxy(_x+5,_y+1); break;     case 6 : gotoxy(_x+9,_y+1); break;     case 7 : gotoxy(_x+1,_y+3); break;     case 8 : gotoxy(_x+5,_y+3); break;     case 9 : gotoxy(_x+9,_y+3); break;

  }
}

int navigate(char a[3][3], int box, int player, int key)

int navigate(char arr[3][3], int box, int player, int key)
{
   switch(key)
   {
     case UPARROW    : if( (box!=1) || (box!=2) || (box!=3) )
		               { box-=3; if(box<1) box = 1; gotobox(box); }
	                   break;

     case DOWNARROW  : if( (box!=7) || (box!=8) || (box!=9) )
		               { box+=3; if(box>9) box = 9; gotobox(box); }
		               break;

     case LEFTARROW  : if( (box!=1) || (box!=4) || (box!=7) )
		               { box--;  if(box<1) box = 1; gotobox(box); }
		               break;

     case RIGHTARROW : if( (box!=3) || (box!=6) || (box!=9) )
		               { box++; if(box>9) box = 9; gotobox(box); }
		               break;

     case ENTER      : if(player == 0)
			            putintobox(arr,'O',box);
		               else if(player == 1)
			            putintobox(arr,'X',box);
		               break;
    }
 return box;
}

int checkforwin(char a[3][3])

int checkforwin(char arr[3][3])
{
  int w=0;


    if((arr[0][0] == arr[0][1]) && (arr[0][1] == arr[0][2])) w = 1;
  else if((arr[1][0] == arr[1][1]) && (arr[1][1] == arr[1][2])) w = 1;
  else if((arr[2][0] == arr[2][1]) && (arr[2][1] == arr[2][2])) w = 1;

    else if((arr[0][0] == arr[1][0]) && (arr[1][0] == arr[2][0])) w = 1;
  else if((arr[0][1] == arr[1][1]) && (arr[1][1] == arr[2][1])) w = 1;
  else if((arr[0][2] == arr[1][2]) && (arr[1][2] == arr[2][2])) w = 1;

    else if((arr[0][0] == arr[1][1]) && (arr[1][1] == arr[2][2])) w = 1;
  else if((arr[0][2] == arr[1][1]) && (arr[1][1] == arr[2][0])) w = 1;

  return w;
}

int boxesleft(char a[3][3])

int boxesleft(char a[3][3])
{
   int i,j,boxesleft=9;

   for(i=0;i<3;i++)
    { for(j=0;j<3;j++)
      { if((a[i][j] == 'X') ||(a[i][j] == 'O'))
          boxesleft--;
      }
    }

   return boxesleft;
}

Now we have all the functions in place, we move on to the third step that talks about Presentation.

We have actually taken care of presentation in some of the above written functions, haven’t we?

The ASCII character used in this program to display the vertical line is 179 and for horizontal line is 196. For a cross — 197. Check this website for more information about extended ASCII characters http://www.asciitable.com/.

You can even printout your ASCII table with a C program. Here is an example:

#include <stdio.h>

int main()
{
    FILE *fh;
    int  ch;
     
    fh = fopen("ascii.txt","r");
 
    for(i=0;i<256;i++)
     fprint(fh,"n%d - %c",i,i);
 
    fclose(fh);
    return 0;
}

Now we will talk about Exception Handling. Here in this program, we have kept it simple so nothing much to worry about here, however, we do take the name from the user. The array we use to store the name is of size 30. What if the user enters a string that is more than 30 in length? This would result in a buffer overflow. This may crash your program right away or produce unexpected results. To avoid this either we can write a function that would get a string from the stdin one by one and stops if Enter is pressed or if string is more than 30 OR we can use the inbuilt function known as fgets.

Finally, putting it all together:

#include <stdio.h>
#include <conio.h>

int main()
{
    
 
    showframe(12,25);
    printf("nPlayer 1, enter your name:"); fgets(name[0], 30, stdin);
    printf("nPlayer 2, enter your name:"); fgets(name[1], 30, stdin);
 
    printf("n%s, you take 0",name[0]);
    printf("n%s, you take X",name[1]); getch();
 
    clrscr();
 
    do
    {
       while(!enter)
       {
         if(khbit())
          ch = getch();
        
           switch(ch)
           {
             case UPARROW : box = navigate(a[3][3], box, player, UPARROW);
             .
             .
             .
           }
       }
       if(quit) break;
              win = checkforwin(a);
    
     }while(!win)
 
    if(win)
    { .
      .
    }
 
    else if(quit)
    {    .
         .
    }
 
 return 0;
}

View the complete source code and executable to take a look at how the program works.

Here are some screenshots of the working executable for Tic Tac Toe:

Image 1

Image 2

Image 3

This article was not a complete fully fledged article for game programming but I hope that you gain something out of it. If you have any questions, please feel free to email me at shine_hack@yahoo.com

Source code for this program can be obtained at: http://techstuff.x10.mx/articles/.

Happy programming!

Shine Jacob (Enot): shine_hack@yahoo.com

I love programming, learn about new technologies, algorithms, and problems solving using programming. I started off programming with C/C++. Though I learned PHP and web development but still stuck on C. Don’t know why but I kinda love it.

Почему-то принято считать, что программировать на C++ тяжело, и изучать язык тоже тяжело, у него много особенностей и так далее. Так вот, это все не совсем правда, и даже совсем не правда. Знать все особенности классно, но совершенно не нужно для того, чтобы делать игры…

Для того чтобы делать игры, достаточно знать о самом языке C++ что программы на C++ состоят из строчек, выполняющихся друг за другом. В строчке программы может стоять метка, и на такую метку можно перескочить из любого другого места в программе при помощи goto. А еще в программе могут быть целочисленные переменные, то есть именованные ячейки памяти в которые можно записать число. Ну и самая сложная из необходимых штука — условный оператор, проверяющий условие и выполняющий блок кода в фигурных скобках только если при проверке условия получилась «истина» или «не ноль». И все. Остальное в С++ можно не знать и при этом писать на С++ по несколько игр каждый день.

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

Итак, если взять хорошую библиотеку, то есть Arctic Engine, то игры на С++ можно делать используя только 3 ключевых слова. Наверное, в это никто не поверит, если я не покажу исходный код пары игр?

Следим за руками, краткий справочник по С++:

goto h;
Оператор безусловного перехода на строку с меткой h.

h:
Метка h.

int x;
Объявление целочисленной переменной x.

if(…) {. . }
Условный оператор, условие ставится в круглых скобках за if, если условие выполняется, то управление передается на оператор (операторы) , стоящий в фигурных скобках за условием. Если же оно не выполняется, то управление переходит к следующей за закрывающейся фигурной скобкой строке программы.

Все слова в приведенных далее программах, не вошедшие в этот справочник — это или имена переменных и меток, или имена функций библиотеки Arctic Engine. Функций библиотеки для создания игр может понадобиться примерно 18 штук, ну и для полного удовольствия еще пригодится строковой тип из стандартной библиотеки C++ (да, в C++ нельзя просто так взять и записать в любую переменную текст, для этого нужно объявить переменную специального библиотечного типа string) . Список нужных функций Arctic Engine с описаниями я опубликую если кто-нибудь попросит в коментариях.

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

Игра «Прыгун через камни»

Код написан исходя из предположения что его будут набирать 1 пальцем люди еще не выучившие английский, поэтому имена переменных однобуквенные. Чтобы как-то компенсировать это приведу описание переменных:
j — счетчик времени до конца прыжка.
b — координата знакоместа камня по горизонтальной оси (направлена слева направо) .
y — координата знакоместа персонажа игрока по вертикальной оси (направлена снизу вверх) .

1 int j = 0
2 int b = 31;
3 s:
4 if (IsKey(» «)) {
5 if (j == 0) {
6 j = 5;
7 }
8 }
9 int y = 0;
10 if (j > 0) {
11 y = 1;
12 j = j — 1;
13 }
14 b = b — 1;
15 if (b < 0) {
16 b = 31;
17 }
18 Cls();
19 At(15, y);
20 Print(«R»);
21 At(b, 0);
22 Print(«O»);
23 if (Screen(15, y) == «O») {
24 At(10, 10);
25 Print(«Game Over»);
26 Input();
27 }
28 Sleep(0.05);
29 Show();
30 goto s;

Всего 30 строчек, из которых 5 состоят только из закрывающейся фигурной скобки! Игра простенькая, однокнопочная, нужно жать пробел чтобы перепрыгивать через камни.

Слишком просто, да? Давайте чуть посложнее игру сделаем! Как насчет игры в которой космический корабль летит слева-направо и уклоняется от метеоритов?

Игра «Метеориты»

p — координата звездолета игрока по вертикальной оси.
x — координата по горизональной оси целевого знакоместа в алгоритме перемещения содержимого экрана справа-налево
y — координата по вертикальной оси целевого знакоместа в алгоритме перемещения содержимого экрана справа-налево

1 int p = 15;
2 b:
3 At(30, Random32(0, 17));
4 Print(«*»);
5 int x = 0;
6 v:
7 int y = 0;
8 h:
9 if (y < 18) {
10 At(x, y);
11 Print(Screen(x + 1, y));
12 y = y + 1;
13 goto h;
14 }
15 x = x + 1;
16 if (x < 31) {
17 goto v;
18 }
19 if (IsKey(7)) {
20 p = p — 1;
21 }
22 if (IsKey(6)) {
23 p = p + 1;
24 }
25 if (Screen(0, p) == «*») {
26 At(10, 10);
27 Print(«Game Over»);
28 Input();
29 Cls();
30 }
31 At(0, p);
32 Print(«>»);
33 Sleep(0.07);
34 Show();
35 goto b;

Совсем другое дело, 35 строчек кода! Наверное, тоже слишком простая игра, и управление всего двумя кнопками, давайте что-нибудь впечатляющее сделаем. Например игру, в которой нужно писать программу для робота, а потом этот робот должен программу выполнять. Придумаем для программирования робота новый язык, который будет состоять только из значков, чтобы проще было его учить. Пусть в программе для робота будут символы <^>V которые будут как бы стрелочками указывающими направление в котором робот должен переместиться на один свой размер. Робота нужно довести до места помеченного крестом.

Игра «Программирование робота»

x — координата робота по горизонтальной оси
y — координата робота по вертикальной оси
p — координата указателя выполняемой роботом инструкции по горизонтальной оси
s — выполняемая инструкция программы робота (с 16 по 27 строки) или содержимое знакоместа, на которое собирается переместиться робот (с 28 по 43 строки)
m — смещение новой позиции робота по горизонтальной оси
n — смещение новой позиции робота по вертикальной оси

1 bbb:
2 int x = 3;
3 int y = 10;
4 int p = 0;
5 At(0, 10);
6 Print(«#n#.#####n#.#…#n#…#X#»);
7 At(x, y);
8 Print(«@»);
9 At(0, 17);
10 Print(«Program:n»);
11 Input();
12 aaa:
13 string s = Screen(p, 16);
14 int m = 0;
15 int n = 0;
16 if (s == «>») {
17 m = 1;
18 }
19 if (s == «<«) {
20 m = -1;
21 }
22 if (s == «V») {
23 n = -1;
24 }
25 if (s == «^») {
26 n = 1;
27 }
28 s = Screen(x + m, y + n);
29 if (s == «@») {
30 At(10, 10);
31 Print(«Game Over!»);
32 ddd:
33 Input();
34 Cls();
35 goto bbb;
36 }
37 At(p, 15);
38 p = p + 1;
39 if (s == «#») {
40 Print(«-«);
41 Beep(1, 0);
42 goto aaa;
43 }
44 Print(«+»);
45 At(x, y);
46 Print(» «);
47 x = x + m;
48 y = y + n;
49 At(x, y);
50 Print(«@»);
51 Sleep(1);
52 Show();
53 if (s == «X») {
54 At(10, 10);
55 Print(«Victory!»);
56 goto ddd;
57 }
58 goto aaa;

Вышло 58 строк.

Итак, для создания игр на C++ достаточно знать всего 3 ключевых слова и двоеточие! Почему я считаю это важным? Потому, что изучить 3 ключевых слова и научиться пользоваться справочником из 18 функций может не только бородатый программист, с этим отлично справляются даже первоклассники, и в результате вместо занудного изучения непонятно зачем нужных возможностей языка на примере абстрактных и скучных задач, можно изучать язык и алгоритмы программируя одну за другой игры в которых есть геймплей.

Один из популярных мотивов обучения программированию — желание написать собственную игру. В этой статье мы сравним несколько игровых движков на практике.

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

Игровые движки для Python обычно являются библиотеками, которые можно установить с помощью менеджера pip или загрузить с площадок распространения кода. В противовес к библиотекам существуют автономные среды, предназначенные исключительно для написания игр:

  • Unreal Engine
  • Unity
  • Godot

Такие программы отличаются от игровых движков Python во многих аспектах:

  • Поддерживаемые языки. Среды программирования игр обычно написаны на C-подобных языках и предлагают использовать соответствующие языки для написания игр: в Unity это C#, в Unreal Engine — C++.
  • Поддержка платформ. Автономные среды позволяют без дополнительных усилий создавать игры для различных платформ, включая мобильные устройства. Напротив, перенос Python-игры на мобильные устройства — задача не из лёгких.
  • Лицензирование. Игры, написанные с использованием автономного игрового движка, имеют особые условия лицензирования и дальнейшего распространения.

Зачем же вообще использовать Python для написания игр? Использование GameDev-сред требует изучения документации и обычно — овладения новым языком программирования. В то же время при работе с игровыми движками на Python питонисты применяют в основном уже имеющиеся знания. Это помогает быстрее двигаться вперед и получить первый результат.

Критерии отбора Python-движков в этой статье:

  • популярность,
  • актуальная поддержка,
  • качественная документация.

Для каждого движка мы рассмотрим способ установки, базовую терминологию и основные возможности для реализации игр.

Pygame

Pygame — первое, что приходит на ум, когда кто-нибудь начинает разговор об играх на Python.

Pygame расширяет собой библиотеку SDL (сокр. от Simple DirectMedia Layer), предназначенную для межплатформенного доступа к мультимедийным аппаратным компонентам системы: мыши, клавиатуре, джойстику, аудио- и видеоустройствам.

Pygame определяет множество классов, которые инкапсулируют концепции, не зависящие от аппаратного обеспечения. В результате игры и мультимедийные программы запускаются в любых системах, где можно установить библиотеку.

Установка Pygame

После создания и активации виртуального окружения установите библиотеку с помощью pip :

        (venv) $ python -m pip install pygame

    

Чтобы проверить результат установки, запустите пример, поставляемый вместе с библиотекой:

        (venv) $ python -m pygame.examples.aliens

    

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

Базовые концепции Pygame

Game loop. Для управления ходом игры используется понятие игрового цикла. Функциональность игрового цикла реализует автор, а Pygame предоставляет необходимые методы и функции. Каждая итерация игрового цикла называется кадром (frame). За один кадр игра выполняет четыре действия:

  1. Обработка пользовательского ввода от мыши, клавиатуры или джойстика с помощью модели событий.
  2. Обновление состояния игровых объектов: спрайтов (образы героев и предметов), изображений, шрифтов и цветов. Объекты описываются подходящими структурами данных или с помощью классов Pygame.
  3. Обновление дисплея и аудиовыхода. Pygame обеспечивает абстрактный доступ к оборудованию для отображения картинки и передачи звука с помощью внутренних модулей display, mixer и music.
  4. Сохранение или изменение скорости игры. Модуль pygame.time позволяет авторам игр контролировать скорость игры. За счёт этого игра работает с одинаковой скоростью на различном оборудовании — библиотека гарантирует завершение каждого кадра в течение заданного периода времени.

Базовый пример Pygame

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

🐍🕹️ Как написать игру на Python: 5 игровых движков

        # Импортируем и инициализируем библиотеку
import pygame

pygame.init()

# Устанавливаем ширину и высоту окна в пикселях
WIDTH = 800
HEIGHT = 600

# Настраиваем окно отрисовки
screen = pygame.display.set_mode([WIDTH, HEIGHT])

# Игровой цикл выполняется, пока пользователь не захочет выйти
running = True
while running:

    # Нажал ли пользователь кнопку зыкрытия окна?
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Заполняем фон белым цветом
    screen.fill((255, 255, 255))

    # Рисуем синий круг в центре экрана радиусом 50
    pygame.draw.circle(screen, (0, 0, 255), (WIDTH // 2, HEIGHT // 2), 50)

    # Рисуем красный контурный квадрат в верхнем левом углу экрана
    red_square = pygame.Rect((50, 50), (100, 100))
    pygame.draw.rect(screen, (200, 0, 0), red_square, 1)

    # Рисуем оранжевый текст с кеглем 60
    text_font = pygame.font.SysFont("any_font", 60)
    text_block = text_font.render(
        "Hello, World! From Pygame", False, (200, 100, 0)
    )
    screen.blit(text_block, (50, HEIGHT - 50))

		# Обновляем экран
    pygame.display.flip()

# Цикл завершился! Уходим.
pygame.quit()

    

Для запуска кода используйте команду:

        (venv) $ python pygame/pygame_basic.py

    

Игровой цикл есть даже в такой скромной программе. В примере он управляется переменной running. Её установка в значение False завершает выполнение программы.

Обработка событий. События хранятся в виде очереди, из неё события извлекаются с помощью pygame.event.get(). В рассматриваемом случае обрабатывается только событие pygame.QUIT, генерируемое при закрытии пользователем окна программы. При обработке этого события мы устанавливаем running = False.

Отрисовка фигур и текста. В то время как для отрисовки фигур просто используются специальные методы, отрисовка текста выглядит несколько сложнее. Сначала выбираем шрифт и создаем объект шрифта. Далее вызываем метод .render() и передаем ему текст, шрифт и цвет. В ответ метод создает объект класса Surface. Этот объект мы копируем на экран screen, используя его метод screen.blit().

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

Продвинутый вариант игры на Pygame

Чтобы лучше изучить возможности Pygame, напишем настоящую игру.

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

При запуске игра выглядит примерно вот так

При запуске игра выглядит примерно вот так
        # Импорт и инициализация
import pygame

# Для случайного размещения монет
from random import randint

# Для поиска ресурсов
from pathlib import Path

# Для аннотации типов
from typing import Tuple

# Устанавливаем размеры окна
WIDTH = 800
HEIGHT = 600

# Как часто должны генерироваться монеты (мс)
coin_countdown = 2500
coin_interval = 100

# Сколько монет должно быть на экране, чтобы игра закончилась
COIN_COUNT = 10

# Определяем спрайт для игрока
class Player(pygame.sprite.Sprite):
    def __init__(self):
        """Инициализирует спрайт игрока"""
        super(Player, self).__init__()

        # Получаем изображение персонажа
        player_image = str(
            Path.cwd() / "pygame" / "images" / "alien_green_stand.png"
        )
        # Загружаем изображение, настраиваем альфа канал для прозрачности
        self.surf = pygame.image.load(player_image).convert_alpha()
        # Сохраняем в прямоугольнике, чтобы перемещать объект
        self.rect = self.surf.get_rect()

    def update(self, pos: Tuple):
        """Обновляет позицию персонажа

        Аргументы:
            pos {Tuple} -- (X,Y) позиция для движения персонажа
        """
        self.rect.center = pos

# Определяем спрайт для монет
class Coin(pygame.sprite.Sprite):
    def __init__(self):
        """Инициализирует спрайт монеты"""
        super(Coin, self).__init__()

        # Получаем изображение монеты
        coin_image = str(Path.cwd() / "pygame" / "images" / "coin_gold.png")

        # Загружаем изображение, настраиваем альфа канал для прозрачности
        self.surf = pygame.image.load(coin_image).convert_alpha()

        # Задаем стартовую позицию, сгенерированную случайным образом
        self.rect = self.surf.get_rect(
            center=(
                randint(10, WIDTH - 10),
                randint(10, HEIGHT - 10),
            )
        )

# Инициализируем движок
pygame.init()

# Настраиваем окно
screen = pygame.display.set_mode(size=[WIDTH, HEIGHT])

# Скрываем курсор мыши
pygame.mouse.set_visible(False)

# Запускаем часы для фиксации времени фрейма
clock = pygame.time.Clock()

# Создаем событие для добавления монеты
ADDCOIN = pygame.USEREVENT + 1
pygame.time.set_timer(ADDCOIN, coin_countdown)

# Настраиваем список монет
coin_list = pygame.sprite.Group()

# Инициализируем счет
score = 0

# Определяем звук для столкновения с монетой 
coin_pickup_sound = pygame.mixer.Sound(
    str(Path.cwd() / "pygame" / "sounds" / "coin_pickup.wav")
)

# Создаем спрайт героя и устанавливаем на заданную позицию
player = Player()
player.update(pygame.mouse.get_pos())

# Цикл событий
running = True
while running:

    # Проверяем, нажал ли пользователь кнопку закрытия окна
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        # Определяем, нужно ли добавлять новую монету
        elif event.type == ADDCOIN:
            # Добавляем новую монету
            new_coin = Coin()
            coin_list.add(new_coin)

            # Ускоряем игру, если на экранее менее 3 монет
            if len(coin_list) < 3:
                coin_countdown -= coin_interval
            # Ограничиваем скорость
            if coin_countdown < 100:
                coin_countdown = 100

            # Останавливаем предыдущий таймер
            pygame.time.set_timer(ADDCOIN, 0)

            # Запускаем новый таймер
            pygame.time.set_timer(ADDCOIN, coin_countdown)

    # Обновляем позицию персонажа
    player.update(pygame.mouse.get_pos())

    # Проверяем, столкнулся ли игрок с монетой и удаляем, если это так
    coins_collected = pygame.sprite.spritecollide(
        sprite=player, group=coin_list, dokill=True
    )
    for coin in coins_collected:
        # Каждая монета стоит 10 очков
        score += 10
        # Воспроизводим звук для монеты
        coin_pickup_sound.play()

    # Проверяем, не слишком ли много монет
    if len(coin_list) >= COIN_COUNT:
        # Если монет много, останавливаем игру
        running = False

    # Указываем цвет фона
    screen.fill((255, 170, 164))

    # Рисуем следующие монеты
    for coin in coin_list:
        screen.blit(coin.surf, coin.rect)

    # Отрисовываем персонажа
    screen.blit(player.surf, player.rect)

    # Выводим текущий счет
    score_font = pygame.font.SysFont("any_font", 36)
    score_block = score_font.render(f"Score: {score}", False, (0, 0, 0))
    screen.blit(score_block, (50, HEIGHT - 50))

    # Отображаем всё на экране
    pygame.display.flip()

    # Скорость обновления - 30 кадров в секунду
    clock.tick(30)

# Готово! Печатаем итоговый результат
print(f"Game over! Final score: {score}")

# Делаем курсор мыши вновь видимым
pygame.mouse.set_visible(True)

# Выходим из игры
pygame.quit()

    

Спрайты в Pygame предоставляют лишь базовую функциональность — в нашем коде мы их расширяем, создавая подклассы для персонажа (Player) и монет (Coin). Объекты спрайтов сохраняются в self.surf и позиционируются с помощью свойства self.rect.

Вывод монет. Чтобы добавлять монеты на экран через равные промежутки времени, мы используем таймер time.set_timer(), отсчитывающий время до события в миллисекундах (coin_countdown). Добавлению монет соответствует событие ADDCOIN. Событие создает новый объект Coin и добавляет его в coin_list. Далее проверяется количество монет на экране. Если монет меньше трех, то coin_countdown уменьшается. Предыдущий таймер останавливается и запускается новый.

Движение персонажа. При движении игрок сталкивается с монетами и собирает их. При этом мы удаляем соответствующий объект из coin_list, обновляем счёт и воспроизводим звук. Автор игры также должен помнить об очистке экрана, отрисовке спрайтов в правильном порядке и отображении счёта.

Заключение по Pygame

Pygame — мощная и хорошо зарекомендовавшая себя библиотека, но у неё есть недостатки. Автор игры должен самостоятельно оформить базовое поведение спрайтов, реализовать игровые циклы и основные обработчики событий. Как вы увидите далее, более современные игровые движки позволяют достичь аналогичных результатов при меньшем объёме выполняемой работы.

Pygame Zero

С одним задачами Pygame справляется хорошо, в других — сказывается возраст библиотеки. Для новичков в написании игр есть вариант получше — Pygame Zero. Эта библиотека разработана для образовательных задач, поэтому текст документации будет понятен даже для новичков в программировании. Кроме того, есть подробное пошаговое руководство.

Установка Pygame Zero

Pygame Zero можно установить, как и любую другую библиотеку Python:

        (venv) $ python -m pip install pgzero

    

Базовый пример на Pygame Zero

Pygame Zero автоматизирует многие вещи, которые программистам приходится описывать вручную при использовании стандартного движка Pygame. По умолчанию Pygame Zero предоставляет создателю игры:

  • игровой цикл — его не нужно писать;
  • модель событий для отрисовки, обновлений и обработки ввода;
  • единую обработку изображений, текста и звука;
  • класс спрайтов и методы анимации для пользовательских спрайтов.

В результате код базовой программы Pygame Zero оказывается более кратким, чем на Pygame.

💡 Чтобы не «раздувать» текст статьи, мы отсылаем читателей посмотреть этот и следующие примеры кода мы в упомянутому выше репозиторию.

Отрисовка окна. Pygame Zero автоматически распознает, что константы WIDTH и HEIGHT относятся к размеру окна. Pygame Zero также предоставляет базовый код для закрытия окна, так что закрытие окна не нужно описывать в обработчике.

Отрисовка фигур и текста. Поскольку Pygame Zero основан на Pygame, он наследует часть кода для отрисовки фигур, но вывод текста выглядит проще и занимает один вызов вместо трех для обычного Pygame.

        screen.draw.text(
        f"Score: {score}",
        (50, HEIGHT - 50),
        fontsize=48,
        color="black",
    )

    

Запуск программ Pygame Zero осуществляется из командной строки с помощью команды:

        (venv) $ python pygame_zero/pygame_zero_basic.py

    

Продвинутый вариант игры на Pygame Zero

Чтобы продемонстрировать разницу между игровыми движками, мы вернемся к той же игре, что описали для Pygame. Игра ведет себя идентично версии, продемонстрированной ранее. Программный код игры также доступен в репозитории.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Спрайты в Pygame Zero называются Actors. Их характеристики требуют некоторых пояснений, так как эти сведения используются в коде примера:

  1. Для каждого Actor задаются, как минимум, изображение и положение.
  2. Все изображения должны располагаться во вложенной папке с именем ./images/. Названия файлов должны содержать лишь строчные буквы, цифры или символы подчеркивания.
  3. При ссылке на изображение используется имя файла без расширения. Например, если изображение называется alien.png, то в программе на него ссылаются строкой "alien" .

Вывод монет через равные промежутки времени производится с помощью метода clock.schedule(). Метод принимает вызываемую функцию (в нашем случае add_coin) и количество секунд перед вызовом самой функции. Запускаемая функция add_coin() создает объект класса Actor и добавляет спрайт в глобальный список видимых монет.

Перемещение персонажа задействует обработчик события on_mouse_move(). В исходном положении спрайт персонажа находится в центре экрана. Положение мыши хранится в глобальной переменной.

Столкновение с монетой приводит к воспроизведению звука и увеличению счёта. Удаляемая монета добавляется в список coin_remove_list. При обработке столкновений мы проверяем, не слишком ли много монет сейчас находится на экране. Если это так, мы заканчиваем игру, прекращаем создавать новые монеты и выводим окончательный счёт игры.

Обновление состояний. Функция update() вызывается Pygame Zero один раз за кадр. Мы используем её, чтобы перемещать объекты класса Actor и обновлять состояния игровых объектов. Также один раз за кадр для отрисовки вызывается функция draw().

Заключение по Pygame Zero

Реализация игры на Pygame Zero заняла 152 строки кода вместо 182 строк на обычном Pygame. Хотя количество строк сопоставимо, версия на Pygame Zero получилась более ясной в плане кода и проще для понимания и дополнения.

Arcade

Arcade — движок Python для создания игр с современными графикой и звуком, разработанный профессором Полом Крэйвеном из Симпсон-колледжа (Айова, США).

Arcade базируется на мультимедийной библиотеке pyglet и выгодно отличается, как от Pygame, так и от Pygame Zero:

  • поддерживает современную OpenGL-графику;
  • поддерживает аннотации типов Python 3;
  • умеет анимировать с анимированными спрайтами;
  • имеет согласованные имена команд, функций и параметров;
  • поощряет отделение игровой логики от кода, обеспечивающего отображение;
  • сокращает использование шаблонного кода;
  • имеет поддерживаемую и актуальную документацию, в том числе несколько учебных пособий и полные примеры игр на Python;
  • имеет встроенные физические движки для игр с видом сверху и игр-платформеров.

Arcade продолжает развиваться и хорошо поддерживается сообществом, а автор быстро реагирует на сообщения об ошибках.

Установка Arcade

Чтобы установить Arcade и его зависимости, используйте соответствующую команду pip:

        (venv) $ python -m pip install arcade

    

Есть также инструкции по установке для Windows, macOS и Linux. При желании можно собрать движок из исходного кода.

Базовый пример на Arcade

🐍🕹️ Как написать игру на Python: 5 игровых движков

Начнем с базового примера с фигурами и текстом. Чтобы запустить код, используйте следующую команду:

        (venv) $ python arcade/arcade_basic.py

    

Начало координат (0, 0) расположено в левом нижнем углу экрана. Это отличает Arcade от большинства игровых движков, в которых начало координат расположено в левом верхнем углу.

Arcade — это объектно-ориентированная библиотека. Для подклассов игр используется класс arcade.Window, для настройки игры вызывается super().__init().

Отрисовка всего что есть на экране производится обработчиком событий .on_draw() . Обработчик стартует с вызова .start_render(), который сообщает Arcade подготовить окно для рисования. Напоминает pygame.flip() для отрисовки в Pygame.

Фигуры и цвета. Каждый из основных методов рисования фигур в Arcade начинается с draw_*. Arcade умеет отрисовывать множество разнообразных фигур и сотни именованных цветов из пакета arcade.color. Также можно использовать кортежи RGB или RGBA.

Продвинутый вариант игры на Arcade

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

Инициализация. Объектно-ориентированный характер Arcade позволяет отделить инициализацию игры от инициализации отдельного уровня: игра инициализируется методом .__init__(), а уровни настраиваются и перезапускаются с помощью .setup(). Это отличный шаблон для использования даже в играх с одним уровнем, как в нашем случае.

Спрайты создаются в виде объекта класса arcade.Sprite, которому задан путь к изображению — Arcade поддерживает pathlib-пути.

Создание монет планируется с помощью arcade.schedule() и вызова self.add_coin() через равные промежутки времени. Метод .add_coin() создает новый спрайт монеты в случайном месте и добавляет его в список.

Перемещение персонажа мышью реализуется с помощью метода .on_mouse_motion(). Метод arcade.clamp() гарантирует, что координаты центра страйпа не выйдут за пределы экрана.

Столкновение с монетой обрабатывается методом .on_update(). Метод arcade.check_for_collision_with_list() возвращает список всех спрайтов, которые столкнулись с указанным спрайтом. Код проходит по этому списку, увеличивает счет и воспроизводит звук. Метод .on_update() проверяет, не слишком ли много монет сейчас есть на экране.

Заключение по Arcade

Реализация Arcade так же удобна для чтения и хорошо структурирована, как и код для Pygame Zero. Однако программный код занимает больше места — 194 строки. Причина в том, что в Arcade заложены возможности, которые лучше реализуются в более крупных играх:

  • анимированные спрайты;
  • встроенные физические движки;
  • поддержка сторонних игровых карт;
  • системы работы с частицами и шейдирование.

Авторы игр, пришедшие в Arcade из Pygame Zero обнаружат здесь знакомую структуру, но более мощные и продвинутые функции.

adventurelib

Мир игр полон самых различных жанров. Бывают и такие игры, как например, Zork, в которых основой игрового повествования является текст. Такой тип игр ещё называют Interactive Fiction. Для создания текстовых игр на Python существует движок adventurelib. Библиотека отлично подойдет для тех, кто хочет создать текстовую игру без необходимости писать парсер языка.

Установка adventurelib

adventurelib доступен на PyPI и может быть установлен с помощью соответствующей команды pip :

        (venv) $ python -m pip install adventurelib

    

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

Базовые концепции adventurelib

Чтобы изучить основы adventurelib, мы рассмотрим небольшую игру с тремя комнатами и ключом, открывающим дверь в последней комнате.

Чтобы запустить код из примера, используйте следующую команду:

        (venv) $ python adventurelib/adventurelib_basic.py

    

Декоратор @when. Текстовые игры в значительной степени полагаются на синтаксический анализ пользовательского ввода. Библиотека определяет текст, который вводит игрок, как команду (command) **и предоставляет для описания команд декоратор @when(). Например, декоратор @when("look") добавляет команду в список допустимых команд и связывает с ней функцией look(). Всякий раз, когда игрок набирает look, adventurelib вызывает соответствующую функцию. Для удобства игроков и разработчиков команды нечувствительны к регистру.

Несколько команд могут использовать одну и ту же функцию. Функция go() декорирована девятью различными командами, чтобы игрок мог перемещаться по игровому миру. В скриншоте ниже есть три из них: south, east, north.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Предметы. В текстовых играх часто есть предметы, которые необходимо собирать, чтобы открывать новые области игры или решать головоломки. Или это могут быть персонажи, с которыми игрок может взаимодействовать. Движок adventurelib предоставляет класс Item для определения и предметов, и персонажей по их именам и псевдонимам.

Конструктор Item()принимает одну или несколько строк. Первая строка — имя предмета, которое используется при печати. Остальные строки используются в качестве псевдонимов, чтобы игроку не приходилось вводить полное имя объекта.

Взаимодействие с предметами. Часто команды, которые вводит игрок, направлены на конкретный предмет. Разработчик игры может задать дополнительный контекст команды. Для этого в описании декоратора @when() используются слова, написанные заглавными буквами.

💡 Пример контекста команды можно увидеть в функции look_at(), которая принимает строковый параметр с именем item. В декораторах этой функции, определяющих команды look at и inspect, слово ITEM выступает в качестве переменной-заполнителя текста, следующего за командой. Этот текст и передается функции look_at() в качестве входного значения. Например, если игрок введет look at book, то item внутри функции получит строковое значение "book".

Реплики. Для вывода реплик используйте функцию say(), которая отлично справляется с выводом многострочного текста. Пример использования есть в теле функции look() — всякий раз, когда игрок набирает look, функция say() выводит в консоль описание текущей комнаты.

Комнаты. Для определения различных областей игрового мира библиотека adventurelib предоставляет класс Room. При создании комнаты конструктору Room() передается описание комнаты и связь с другими комнатами с помощью свойств .north, .south, .east и .west.

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

Ключ имеет не только имя и псевдонимы, но и метод, с помощью которого используется в игре. Метод key.use_item вызывается, когда игрок пытается использовать предмет, набрав строку use key.

Коллекции предметов, таких как инвентарь игрока или предметы на полу комнаты, хранятся в объекте класса Bag. Эти объекты-коллекции позволяют добавлять, удалять предметы или проверять содержимое.

В нашем коде определены четыре Bag-объекта: три для каждой комнаты и один для инвентаря, собираемого игроком. Для добавления предметов в инвентарь используется функция get(), Чтобы взять какой-либо предмет из имеющегося инвентаря — функция take(). При переносе предмета в инвентарь, он удаляется из Bag-объекта комнаты.

Продвинутый вариант игры с использованием adventurelib

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

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

В игре есть несколько областей:

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

Итак, есть несколько предметов для сбора, включая еду и оружие. И есть персонажи, с которыми можно взаимодействовать. Ещё нужно запрограммировать систему боя, чтобы игрок мог сразиться с великаном.

Программный код игры разбит на несколько файлов:

  • adventurelib_game_rooms.py описывает комнаты и области;
  • adventurelib_game_items.py определяет предметы и их атрибуты;
  • adventurelib_game_characters.py описывает персонажей, с которыми может взаимодействовать игрок;
  • adventurelib_game.py собирает всё вместе, добавляет команды и запускает игру.

Игру можно запустить с помощью следующей команды:

        (venv) $ python adventurelib/adventurelib_game.py

    

В предыстории мы наметили игровые области и пути, по которым может перемещаться игрок. Они показаны на блок-схеме ниже.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Каждая область имеет свои свойства:

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

Используем ООП. Чтобы быть уверенными, что каждая область имеет собственный экземпляр каждого из этих свойств, мы создадим в файле у подкласс Room с именем GameArea. Предметы в каждой комнате хранятся в объекте класса Bag с именем items, а персонажи — в characters.

Игровые предметы определены в adventurelib_game_items.py как объекты типа Item(). Одни игровые предметы необходимы для завершения игры, в то время как другие разнообразят геймплей. Некоторые элементы имеют определенные свойства, уникальные для данного элемента. Например, у мечей wooden_sword и steel_sword есть свойство, показывающее наносимый ими урон и поддерживаемые магические бонусы.

Взаимодействие с персонажами помогает продвигать игровой сюжет. Персонажи определены в adventurelib_game_characters.py. У каждого героя, как и у каждого предмета, есть связанные с ним универсальные свойства, такие как описание и приветствие, используемое, когда игрок встречает персонажа в первый раз.

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

Примеры контекста. В какой-то момент игрок впервые сталкивается со старейшиной Бэрроном (Elder Barron). Когда игрок набирает talk to elder, контекст устанавливается по свойству elder.context. Приветствие старейшины заканчивается вопросом требующим ответа «да» или «нет». Если игрок вводит yes, то в adventurelib_game.py запускается обработчик команды, заданный декоратором @when("yes", context="elder").

🐍🕹️ Как написать игру на Python: 5 игровых движков

Позже при разговоре игрока с кузнецом добавляется второй уровень контекста, отражающий возможную покупку оружия. За счет этого программа понимает, что одна и ту же команда "yes" приводит к разным последствиям.

Запрет действий в контексте. Вы также можете проверить, как помогает контекст в обработчике команд. Например, игрок не может просто выйти из боя с великаном, закончив разговор. Обработчик команды "goodbye" проверяет, находится ли игрок в контексте "giant", который вводится, когда он начинает сражаться. Если контекст в силе, прекращать разговор нельзя, — это смертельный бой!

Команды, не имеющие совпадений, обрабатываются функцией no_command_matches(). Её можно использовать для диалогов, требующих конкретного ответа. Так, когда волшебник просит игрока разгадать загадку, создается контекст wizard.riddle. Неправильный ответ приводит к прекращению разговора.

Заключение по adventurelib

Создать полноценную текстовую приключенческую игру непросто, но adventurelib берёт на себя основную рутину, связанную с обработкой локаций, предметов, героев, а также их поведением в зависимости от истории взаимодействий.

Ren’Py

Наследником текстовых игр являются современные игры в стиле визуальных романов (новелл). В таких играх наибольшее значение также играют повествование и сюжет, но игровой опыт разнообразнее за счет картинок, визуальных эффектов и звуков. Для создания подобных игр на Python используется Ren’Py. Название движка образовано от японского слова, означающего романтическую любовь.

Строго говоря, Ren’Py не является классической библиотекой Python, которую можно установить посредством pip install. Игры Ren’Py создаются и запускаются с помощью лаунчера, входящего в состав SDK Ren’Py, в котором есть и редактор игр, и собственный язык сценариев. Однако так как Ren’Py основан на Pygame, с ним можно работать и с помощью Python.

Установка Ren’Py

Ren’Py SDK доступен на Windows, Mac и Linux, пакет для соответствующей платформы можно скачать на официальном сайте. После установки перейдите в папку, содержащую SDK, и запустите Ren’Py Launcher.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Базовые концепции Ren’Py

В той же программе можно начать новый проект, это создаст необходимую структуру файлов и папок. Хотя для запуска игр требуется Ren’Py Launcher, для редактирования кода можно использовать любой удобный редактор.

🐍🕹️ Как написать игру на Python: 5 игровых движков

Сценарии игр Ren’Py хранятся в файлах с расширением .rpy , написанных на специальном языке Ren’Py. Файлы хранятся в папке game/ внутри папки проекта.

Для нового проекта Ren’Py создаёт следующие сценарии, которые можно сразу же использовать и редактировать:

  • gui.rpy определяет внешний вид всех элементов пользовательского интерфейса;
  • options.rpy определяет изменяемые параметры для настройки игры;
  • screens.rpy описывает стили диалогов, меню и других элементов вывода информации;
  • script.rpy — место, где вы начинаете писать игру.

Чтобы запустить примеры игры из материалов этого руководства, следуйте инструкции:

  1. Запустите Ren’Py Launcher.
  2. Нажмите Preferences, затем Projects Directory.
  3. Измените Projects Directory на папку renpy из загруженного репозитория с примерами.
  4. Нажмите Return, чтобы вернуться на главную страницу Ren’Py Launcher.

В списке проектов слева вы увидите basic_game и giant_quest_game. Выберите, что хотите запустить и нажмите Launch Project.

Ниже мы рассмотрим script.rpy для basic_game.

Метки (labels) **определяют точки входа в историю, а также используются для запуска новых сцен и альтернативных путей прохождения истории. Все игры Ren’Py начинают работать с метки start:, которая может появляться в любом сценарии.

💡 Вы также можете использовать метки для определения фоновых изображений, настройки переходов между сценами и управления внешним видом персонажей. В примере вторая сцена начинается со строки с меткой check_room:.

Персонажей можно определить просто назвав их в истории или указав в начале сценария. Так мы определили главного персонажа, его маму и брата по имени Кевин. Оператор define инициализирует три переменные класса Characters.

Медиафайлы. Как и Pygame Zero, Ren’Py требует, чтобы все изображения и звуки, используемые в игре, находились в определенных папках — соответственно game/images/ и game/audio/. В сценарии игры к ним можно обращаться по имени файла без расширения.

💡 Пример. Когда ваш персонаж открывает глаза и впервые видит спальню, ключевое слово scene очищает экран, а затем показывает изображение спальни, хранящееся в day.png. Ren’Py поддерживает формы файлов изображений JPG, WEBP и PNG.

Ветвления сюжета. Игра не была бы игрой, если бы в ней нельзя было принимать решения. В Ren’Py возможности для выбора оформляются в виде меню. В ответ на выбор игра переходит к заданной метке, изменяет изображение и воспроизводит заданные звуки. В примере такой выбор возникает, когда главный персонаж понимает, что забыл свой телефон.

Продвинутый пример игры на Ren’Py

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

Персонажи и локации. В игре есть несколько персонажей: кузнец, волшебник и великан. И несколько локаций: родная деревушка, деревня оружейника, ведущая к волшебнику дорожка и пещера с грабителем-великаном.

В материалах к туториалу вы найдете четыре сценария:

  • script.rpy, где начинается игра;
  • town.rpy с историей близлежащей деревни;
  • path.rpy , который описывает тропу между деревнями;
  • giant.rpy, содержащий логику битвы с великаном.

Встреча с волшебником оставлена в качестве упражнения.

Описание объектов. Как и в предыдущем примере, в начале script.rpy мы определяем объекты с помощью Character(). Далее мы задаем несколько изображений, на которые ссылаемся позже для использования в качестве фона и отображения объектов. Использование специального синтаксиса позволяет назначать изображениям короткие внутренние имена.

Активный инвентарь. Чтобы показать, какое оружие сейчас активно, мы показываем его изображение в углу экрана. Для этого мы используем команду show с модификатором with moveinleft. Важно помнить, что при смене сцены экран очищается, поэтому нужно запускать команду повторно.

Смена оружия. При входе в город в town.rpy, вы встречаете приветствующего вас кузнеца:

🐍🕹️ Как написать игру на Python: 5 игровых движков

Кузнец предлагает улучшить ваше оружие. Если вы решите это сделать, вы обновите значения для current_weapon и характеристики оружия.

Операторы Python. Строки, начинающиеся с символа $, интерпретируются Ren’Py как операторы Python. Это позволяет прописывать в сценарии произвольный код Python. Обновление current_weapon и статистики оружия выполняется с помощью трех операторов Python, которые изменяют значения переменных по умолчанию, определенных в начале script.rpy.

Вы также можете определить большой блок кода Python, используя слово python:, как показано в файле giant.rpy, начиная со строки 41.

Сцена битвы управляется функцией fight_giant() и игровым циклом с переменной battle_over. Выбор игрока сражаться или бежать отображается с помощью метода renpy.display_menu(). Если игрок сражается, то великану наносится случайное количество урона и корректируются его очки здоровья. Если великан остается в живых, он может атаковать в ответ аналогичным образом. Обратите внимание, что у великана есть шанс промахнуться, в то время как игрок всегда попадает в цель. Бой продолжается до тех пор, пока у игрока или великана не закончится здоровье, либо пока игрок не сбежит.

Используемый код очень похож на тот, который мы использовали для описания битвы в adventurelib. Пример демонстирирует, как вы можете интегрировать код Python в Ren’Py без необходимости переводить его в сценарий Ren’Py.

Если вы заинтересовались движком, обратитесь к документации Ren’Py для получения более подробной информации.

Другие популярные игровые движки на Python

Описанная в статье пятерка библиотек — лишь небольшая выборка из множества доступных игровых движков на Python. Среди десятков доступных мы отметим также следующие:

  • Wasabi 2D разработан командой Pygame Zero. Это современная среда, построенная на moderngl , которая автоматизирует рендеринг, предоставляет готовые решения для анимационных эффектов, имеет встроенные эффекты и использует собственную модель игровых событий.
  • Panda 3D — платформа с открытым исходным кодом для создания 3D-игр и трехмерной визуализации. Panda 3D переносится на разные платформы, поддерживает несколько типов ресурсов, интегрируется с многочисленными сторонними библиотеками и обеспечивает встроенное профилирование.
  • Ursina построена на основе Panda 3D и предоставляет специальный движок для разработки игр, который упрощает многие аспекты Panda 3D. На момент написания статьи Ursina хорошо поддерживается и документируется.
  • PursuedPyBear позиционируется как образовательная библиотека с системой управления сценами, анимированными спрайтами и низким входным барьером.

Если вы знаете о других хороших движках на Python, не стесняйтесь рассказать в комментариях!

Источники контента для игр

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

Разработчики-одиночки могут посчитать этот аспект разработки игр пугающим. Но к счастью, существует множество различных источников игровых ресурсов:

  • OpenGameArt.org предлагает широкий спектр артов, музыки, фонов, значков и других ресурсов для двумерных и трехмерных игр. Большинство файлов находятся в свободном доступе.
  • Kenney.nl содержит набор разнообразных бесплатных и платных ресурсов.
  • Itch.io — торговая площадка для создателей цифровых продуктов, ориентированных на независимую разработку игр. Здесь можно найти ресурсы практически для любых целей: и бесплатные, и платные, и даже готовые игры.

Все ресурсы, используемые в играх из этого туториала, соответствуют лицензионным требованиям создателей.

Заключение

Поздравляем — теперь вы знакомы с основами дизайна игр на Python! Благодаря стараниям GameDev-сообщества писать качественные компьютерные игры на Python сегодня намного проще, чем раньше.

Надеемся, что теперь вы можете выбрать подходящий игровой движок и написать собственную игру.

***

Материалы по теме

  • 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame
  • 9 идей для начинающих программистов по созданию игр-клонов

Понравилась статья? Поделить с друзьями:
  • Как пишут когда умирает человек
  • Как пишут когда соболезнуют
  • Как пишут книжки
  • Как пишут книги современные писатели
  • Как пишут книги план