Автор: Корягин А.
Теги: компьютерная графика специализированные и управляющие электронные вычислительные машины дискретного действия операционные системы компьютер компьютерные игры издательство питер серия программирование для детей
ISBN: 978-5-4461-2179-3
Год: 2024
АНДРЕЙ КОРЯГИН
ROB
OX
В ДЕЙСТВИИ
ИСКУССТВО РАЗРАБОТКИ ИГР
2024
Корягин Андрей
К70 R
oblox в действии. Искусство разработки игр. — СПб.: Питер,
2024. — 304 с.: ил. — (Серия «Программирование для детей»).
ISBN 978-5-4461-2179-3
Roblox — это огромная платформа для создания игр, не похожая ни на одну
другую. Roblox позволяет новым разработчикам создавать игры, в которые может
играть весь мир.
Книга сочетает теорию с практикой, поскольку с разработкой игр связаны множество смежных дисциплин: программирование, 3D-моделирование и анимация, работа
со звуком, написание сценария, художественное оформление (level design), маркетинг
и многое другое. Мы будем создавать логику поведения персонажей и некоторых
объектов в игре. Чтобы твоя игра была уникальна, нужно научиться создавать свои
игровые объекты: 3D-модели, звуки, изображения и текстуры. Проектируя элементы
игры, ты наберешься опыта и отточишь навыки разработчика игр, гейм-дизайнера,
звукорежиссера и программиста.
Игры, которые мы будем создавать, могут запускаться в любых распространенных
операционных системах: Windows, MacOS, iOS, Android и Xbox One, а значит, в них
смогут сыграть очень много людей. В среде Roblox Studio заложены не только кросс
платформенность, но и мультиплеер, позволяющий подключаться к игре множеству
игроков.
16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ББК 32.973.23-018.9
УДК 004.928
Все права защищены. Никакая часть данной книги
не может быть воспроизведена в какой бы то ни было
форме без письменного разрешения владельцев
авторских прав.
Информация, содержащаяся в данной книге,
получена из источников, рассматриваемых
издательством как надежные. Тем не менее, имея
в виду возможные человеческие или технические
ошибки, издательство не может гарантировать
абсолютную точность и полноту приводимых
сведений и не несет ответственности за возможные
ошибки, связанные с использованием книги.
ISBN 978-5-4461-2179-3
© ООО Издательство «Питер», 2024
© Серия «Программирование для детей», 2024
© Андрей Корягин, 2024
Издательство не несет ответственности
за доступность материалов, ссылки на которые
вы можете найти в этой книге. На момент подготовки
книги к изданию все ссылки на интернет-ресурсы
были действующими.
ОГЛАВЛЕНИЕ
Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Глава 1. Знакомство с Roblox Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Регистрация в Roblox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Установка Roblox Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Структура среды разработки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Глава 2. Игра: структура и технологии . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Что такое игра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Камера, свет, атмосфера . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Сбор, разрушение, ремонт, лечение и нанесение урона . . . . . . . . . . . . . 33
Движение, анимация, симуляция (физика и эффекты) . . . . . . . . . . . . . . 57
Игровой опыт . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Звуки, звуковые эффекты в играх . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Графические интерфейсы пользователя и диалоги . . . . . . . . . . . . . . . . . 105
Генерация игровых объектов и целых игровых миров . . . . . . . . . . . . . . 111
Разработка сложных структур игровых объектов и локаций . . . . . . . . 122
Импорт 3D-моделей, анимации и текстур . . . . . . . . . . . . . . . . . . . . . . . . 134
Экономика в играх: игровые товары, покупка, инвентарь . . . . . . . . . . 143
Глава 3. Платформер: структура и создание . . . . . . . . . . . . . . . . . . . . . . . 161
Платформер — бегущий по лезвию игры . . . . . . . . . . . . . . . . . . . . . . . . . 162
Формирование игровой истории: мотивация игрока.
Начало и конец . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Разработка игровой локации. Создание правил, базовой
структуры и логики . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Разработка интерфейса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Звуковое сопровождение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
Тестирование, отладка и публикация игры . . . . . . . . . . . . . . . . . . . . . . . 220
4
Глава 4. Песочница: творческая игра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Творчество ради творчества: анализ жанра . . . . . . . . . . . . . . . . . . . . . . . 226
Разработка локации и правил . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Разработка игровых инструментов для творчества . . . . . . . . . . . . . . . . . 239
Разработка графического интерфейса . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
Глава 5. Создание игрового магазина . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
Игровая валюта: внутренняя и глобальная . . . . . . . . . . . . . . . . . . . . . . . 279
Разработка товаров для продажи в сообществе Roblox . . . . . . . . . . . . . 281
Продаем товары внутри игры за Robux . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
Послесловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
Предметный указатель . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
ВВЕДЕНИЕ
Привет, дорогой читатель и творческая личность! Если ты горишь желанием
научиться создавать игры на платформе Roblox и погрузиться в профессию
разработчика игр, то эта книга для тебя. Это продвинутое руководство по
созданию игр разных жанров: платформера, песочницы, стратегии.
Книга сочетает теорию с практикой, поскольку с разработкой игр связаны
множество смежных дисциплин: программирование, 3D-моделирование
и анимация, работа со звуком, написание сценария, художественное оформление
(level design), маркетинг и многое другое.
Некоторые темы простые, а некоторые — сложные. Поэтому в книге я буду давать ссылки на дополнительную информацию по теме. А если ты совсем новичок в программировании, 3D-моделировании и анимации в Roblox или других
средах, то рекомендую сначала почитать книгу «Roblox. Играй, программируй
и создавай свои миры»1. Книга, которую ты сейчас держишь в руках, — это
продолжение предыдущей, поэтому основы в ней даны по минимуму. Здесь мы
займемся созданием красочных и полноценных игр в Roblox.
Первая глава будет посвящена реализации некоторых игровых технологий,
а также структуре игр: механике, технологии, эстетике и сюжету. В последующих
главах мы перейдем к реализации и скомбинируем все технологии в единую
структуру — игру.
Мы будем создавать логику поведения персонажей и некоторых объектов в игре.
Чтобы твоя игра была уникальна, нужно научиться создавать свои игровые
объекты: 3D-модели, звуки, изображения и текстуры. Проектируя элементы игры,
ты наберешься опыта и отточишь навыки разработчика игр, гейм-дизайнера,
звукорежиссера и программиста.
Игры, которые мы будем создавать, могут запускаться в любых распространенных
операционных системах: Windows, MacOS, iOS, Android и Xbox One, а значит,
в них смогут сыграть очень много людей. В среде Roblox Studio заложены не
только кроссплатформенность, но и мультиплеер, позволяющий подключаться
к игре множеству игроков.
Многие примеры кода из книги и целые локации ты можешь скачать со страницы
GitHub: https://github.com/Antipat/Game-Dev-Roblox.
1
Корягин А. «Roblox: играй, программируй и создавай свои миры». 2-е изд. Санкт-Петербург, издательство «Питер».
1
ЗНАКОМСТВО
С ROBLOX
STUDIO
В этой главе мы рассмотрим программы компании Roblox
и их инструментарий. Изучим процедуру регистрации,
установки приложений на компьютер и телефон. Познакомимся с инструментами Roblox Studio на примере
создания локации.
8
РЕГИСТРАЦИЯ В ROBLOX
Roblox — многопользовательская онлайн-платформа, на которой можно играть,
создавать игры и размещать их. Игры разрабатываются исключительно в Roblox
Studio. Играть можно только в игры, созданные на этом игровом движке. Таким
образом, разработка игр и их выбор ограничиваются возможностями движка
Roblox Studio и правилами самой компании.
Но Roblox дает и множество преимуществ: легкая публикация игр и несложная
разработка, многоплатформенность для охвата большого количества пользователей, неограниченность жанров за исключением тех, которые противоречат
политике организации.
Для начала работы нужно зарегистрироваться в Roblox. Для этого перейди на
сайт платформы.
РИС. 1.1. ЗАХОДИМ В ROBLOX
Если ты впервые на сайте Roblox, тебе предложат создать аккаунт. Это позволит
использовать все доступные инструменты, а также привязывать игры к собственному аккаунту и размещать их в сообществе Roblox. Также аккаунт нужен, чтобы
хранить свои достижения в играх, в которые ты играешь.
Если ты уже указал дату рождения и пол, придумал имя для аккаунта и пароль,
нажимай на кнопку Sign up. В некоторых случаях нужно пройти аутентификацию
на подтверждение, что ты не бот. Обычно это девять картинок, где нужно выбрать правильное изображение, или фигурка животного, которую нужно быстро
вращать так, чтобы расположить горизонтально в естественном положении.
Нажимай на стрелки по бокам и вращай картинку.
9
РИС. 1.2. РЕГИСТРАЦИЯ В ROBLOX
РИС. 1.3. ПРОВЕРКА НА БОТА
10
После успешной регистрации ты окажешься в личном кабинете Roblox. В зависимости от выбранного пола тебе предложат шаблон аватара, который в дальнейшем и будет загружаться в виде игровой модели.
РИС. 1.4. ЛИЧНЫЙ КАБИНЕТ ПОЛЬЗОВАТЕЛЯ
Структуру кабинета я подробно описал в книге «Roblox. Играй, программируй
и создавай свои миры». Это не связано с созданием игр как таковым, и об этом
я расскажу позже.
11
УСТАНОВКА ROBLOX STUDIO
Теперь скачай и установи среду разработки игр — Roblox Studio. Для этого перей
ди на вкладку Create (рис. 1.5).
РИС. 1.5. ДОСТУП К ROBLOX STUDIO
Ты перейдешь на страницу загрузки и увидишь кнопку Start Creating (рис. 1.6).
Щелкни по ней и дождись, когда загрузится установщик. По умолчанию все скачанные файлы хранятся в папке Загрузки (Downloads).
РИС. 1.6. СТРАНИЦА ЗАГРУЗКИ ROBLOX STUDIO
12
Запусти установщик и дождись полной установки программы.
РИС. 1.7. УСТАНОВЩИК ROBLOX STUDIO
Когда ты создашь свой первый проект и сохранишь его, страница Create изменится. Тебе будут доступны созданные проекты для дальнейшего редактирования
и использования. На рис. 1.8 показана эта страница. Вот что на ней есть:
1. Вкладки, ведущие на страницу разработчика и проекты, документацию Roblox
Studio, страницу магазина, страницу поиска разработчиков, на форум и страницу последних обновлений.
РИС. 1.8. СТРАНИЦА РАЗРАБОТЧИКА
13
2. Выбор проектов разработчика. Обычно там есть ссылка на твой профиль
и группы, от лица которых можно создавать игры.
3. Кнопка скачивания или запуска Roblox Studio, а также переход непосредственно в проект.
4. Твои последние игровые проекты.
Эта страница — ценный ресурс для твоих проектов, поскольку это менеджер всех
созданных тобой игровых активов. Здесь можно получить быстрый доступ к ним
для настройки и продвижения.
Следующий шаг — знакомство со средой разработки Roblox Studio. Когда ты
поймешь ее структуру, тебе будет легче создавать игры, плагины или просто
игровые элементы.
СТРУКТУРА СРЕДЫ РАЗРАБОТКИ
Если программа установлена, то на твоем рабочем столе будет значок в виде
синего квадрата с названием Roblox Studio. Щелкни по нему, чтобы открыть
программу (рис. 1.9).
РИС. 1.9. ЗАГРУЗОЧНОЕ ОКНО ROBLOX STUDIO
Ты увидишь окно с четырьмя вкладками сбоку слева. По умолчанию открыта
вкладка New (рис. 1.9). Здесь можно выбрать шаблон для своей игры. На первых
порах рекомендую использовать первые три шаблона, так как они не загружены
игровыми элементами.
14
Если ты не знаешь, какая будет игра, или предполагаешь, что она будет сильно
отличаться от шаблонов, то, скорее всего, тебе придется удалять ненужные элементы из шаблонов. Но и игнорировать их не стоит — там представлены интересные игровые решения, которые помогут в разработке игры.
Вкладка My Games содержит список твоих игр, которые ты публикуешь в сообществе Roblox. Игры могут быть как общедоступными, так и ограниченными в доступе (рис. 1.10). Если созданных игр еще нет, то окно будет пустым.
РИС. 1.10. СОЗДАННЫЕ И ОПУБЛИКОВАННЫЕ ИГРЫ
Вкладка Recent содержит список сохраненных и открываемых проектов. Здесь
также содержится список проектов, которые не публиковались в Roblox, а хранятся только на компьютере (рис. 1.11).
РИС. 1.11. СПИСОК НЕДАВНО ОТКРЫТЫХ ПРОЕКТОВ
15
На вкладке Arhive хранятся проекты, которые ты решишь заархивировать (очистить место на сервере Roblox) и сделать недоступными всем остальным пользователям.
Вернемся во вкладку New, создадим первый проект из шаблона Baseplate и познакомимся с интерфейсом программы (рис. 1.12).
РИС. 1.12. ПРОЕКТ BASEPLATE
Среда разработки состоит из множества окон, часть из них — основные. В первую
очередь, это верхняя панель управления с основным меню, центральное окно
с визуализацией проекта и боковые окна Explorer и Properties. Остальные окна
дополнительные и нужны для упрощения работы.
Рассмотрим поочередно каждое окно и начнем с верхней панели инструментов.
По умолчанию активно меню Home со своей панелью (рис. 1.13).
РИС. 1.13. МЕНЮ HOME
16
Меню содержит базовый набор инструментов:
копирование, вырезание и вставка;
инструменты перемещения игровых объектов (смещение, вращение, масштабирование);
редактор локации (создание поверхности);
готовые инструменты (меши, модели, изображения и т. д.);
примитивы: куб, сфера, цилиндр, пирамида, скос;
инструменты графического интерфейса пользователя;
материал (создание текстур);
палитра цветов;
инструменты физики и ограничения;
инструменты отладки, запуска и публикации проекта.
Меню Model содержит схожий с Home инструментарий, но с дополнительными
средствами для точного редактирования игровой модели (рис. 1.14):
точное позиционирование в смещении и вращении;
точный выбор точки приложения (точка воздействия двух взаимодействующих объектов);
инструменты моделирования;
инструменты связывания игровых объектов и их ограничение, а также приложение физических величин;
эффекты: свет, огонь, взрыв, частицы, дым и т. д.;
инструменты логики и физики.
РИС. 1.14. МЕНЮ MODEL
Меню Avatar содержит инструменты для импорта 3D-моделей, создания
3D-персонажей и аксессуаров, а также анимации (рис. 1.15).
17
РИС. 1.15. МЕНЮ AVATAR
Меню Test содержит инструменты для тестирования игрового проекта на сервере
(рис. 1.16). Здесь ты тестируешь игру с несколькими игроками и проводишь отладку. Также здесь проходит тестирование на симуляторах различных устройств:
мобильных и стационарных.
РИС. 1.16. МЕНЮ TEST
Меню View содержит функциональность по отображению окон редактора Roblox
Studio. Есть здесь и дополнительные инструменты для проектирования и отладки,
а также инструменты для записи твоих действий на экране проекта и создания
скриншотов (рис. 1.17).
РИС. 1.17. МЕНЮ VIEW
Меню Plugins содержит инструменты для импорта и экспорта плагинов (рис. 1.18).
РИС. 1.18. МЕНЮ PLUGINS
Есть окно Insert Object, которое лежит в меню Model. С его помощью можно добавить внутренний элемент для игрового объекта. Чтобы добавить элемент из
Insert Object, выдели игровой объект на сцене и щелкни по выбранному элементу.
Для каждого объекта могут быть свои элементы.
18
РИС. 1.19. ОКНО INSERT OBJECT
Окно Insert Object — это дополнительная функция, ее задачи дублируются в окне
Explorer (рис. 1.20).
РИС. 1.20. ОКНО EXPLORER
В окне Explorer отображается структура твоего проекта. Не менее важно окно
Properties, оно содержит список свойств, которыми обладает выделенный игровой
объект (рис. 1.21). Многие параметры свойства можно редактировать.
19
РИС. 1.21. ОКНО PROPERTIES
Еще два вспомогательных окна — Toolbox и Material — нужны для добавления
уже готовых игровых объектов и текстур. Также здесь есть окно отладки Output
(рис. 1.22–1.23).
РИС. 1.22. ОКНО TOOLBOX
20
РИС. 1.23. ОКНА OUTPUT И MATERIAL MANAGER
Почти весь инструментарий Roblox Studio описан в книге «Roblox. Играй, программируй и создавай свои миры». Там же описаны основы программирования, 3D-моделирования и разработки базовых игровых механик. В этой книге
ты будешь создавать игры, программировать, моделировать и разрабатывать
логику уже на более продвинутом уровне.
2
ИГРА:
СТРУКТУРА
И ТЕХНОЛОГИИ
В этой главе мы изучим разработку базовых игровых механик: сбор предметов, нанесение урона, лечение, разрушение, анимация, обработка событий, диалоги, эффекты,
хранение данных игрока, hud и создание магазина, а также
основы гейм-дизайна: работа с окружением, подсказки, разработка локаций, импорт 3d-моделей.
22
ЧТО ТАКОЕ ИГРА
Чтобы начать создавать игры, нужно разобраться в самом понятии игры. Однозначного определения нет, но вот несколько вариантов1:
Игра — это один из видов деятельности, характерных для животных и человека, она имитирует обстановку предметной и социальной действительности.
Игра — это соревнование или состязание между участниками (детьми или
взрослыми) по заранее согласованным, строго определенным правилам (условиям), направленным на достижение определенных общепринятых целей.
Игра — форма деятельности в условных ситуациях, направленная на воссоздание и усвоение общественного опыта, фиксированного в социально
закрепленных способах осуществления предметных действий, в предметах
науки и культуры.
Игра — это занятия, действия, формы общения детей, не носящие обязательного характера, приносящие чувство радости, удовольствия от достижения
игрового результата.
Определений много, и все они разные, но из них можно взять ключевые фразы:
обучение, социализация;
досуг, развлечение, эмоции;
правила, получение результата;
получение опыта и навыков.
Определившись с ключевыми фразами, перейдем к определению компьютерной
игры.
Компьютерная игра — это компьютерная система, способствующая такого рода
досугам или социальным взаимодействиям, каковыми характеризуется понятие
«игра» и явление спортивных игр в частности2.
Как видно, понятие «компьютерная игра» опирается на фундаментальное понятие игры, но ограниченной рамками вычислительной техники (компьютеры,
приставки, планшеты, телефоны и т. д.).
Таким образом, хорошая компьютерная игра должна:
вызывать эмоции (желательно положительные);
содержать органичную игровую механику, определяемую правилами игры;
давать игровой опыт для роста персонажа в его мире;
содержать небольшие и крупные цели, которые хочется достигать для получения игрового опыта и достижения результата.
1
2
Источник: https://vocabulary.ru/termin/igra.html
Определение взято из https://cyclowiki.org
23
Если стараться следовать этим правилам, то твоя игра, скорее всего, будет интересна многим пользователям. Не буду углубляться в теорию разработки игр, так
как об этом написано немало книг, а сразу перейду к ее практической реализации.
На примерах мы рассмотрим правдивость теории, описанной выше.
КАМЕРА, СВЕТ, АТМОСФЕРА
При выборе жанра игры нужно определиться с концепцией проекта: о чем будет игра, какие у нее правила, персонажи и механики. От этого зависит выбор
работы камер, а также настройка света и окружения для создания атмосферы
погружения в историю.
КАМЕРА
Камеру, свет и атмосферу можно настраивать как вручную, так и с помощью
скриптов.
Начнем с настройки камеры. Когда игрок попадает в игру, то по умолчанию он
уже имеет камеру, благодаря которой и видит окружение. Стандартные настройки камеры находятся в папке StarterPlayer в окне Explorer. Щелкнув по ней, откроем
ее свойства, которые сможем настроить в окне Properties (рис. 2.1). Эти свойства
определяют настройки персонажа в игре: место загрузки персонажа, характеристики прыжка, скорости перемещения, способы управления и настройка камеры.
РИС. 2.1. НАСТРОЙКА ПАРАМЕТРОВ ИГРОКА
24
Рассмотрим подробнее параметры свойства Camera.
Для настройки максимального приближения и удаления камеры используют два
параметра: CameraMaxZoomDistance и CameraMinZoomDistance.
Параметр CameraMode имеет два варианта: Classic и LockFirstPerson. В первом разрешается изменение масштаба камеры и вращение вокруг игрока, а во втором вид
только от первого лица. Дополнительно можно настроить ограничения камеры
для игр на компьютере и мобильных устройствах: DevComputerCameraMovementMode
и DevTouchCameraMovementMode. Можно настроить и поведение камеры, если между игроком и камерой возникает препятствие: камера приближается к игроку, камера видит сквозь препятствие. За это отвечает параметр DevCameraOcclusionMode.
Подробно об этих настройках можно почитать здесь: https://developer.roblox.com/
en-us/articles/customizing-the-camera.
Допустим, нужно, чтобы вид для всех игроков был от третьего лица, без возможности изменять, приближать и удалять камеру. Для этого укажем одинаковые
значения для минимальной и максимальной дистанции камеры (рис. 2.2).
РИС. 2.2. ВИД ОТ ТРЕТЬЕГО ЛИЦА
Более специфичные варианты настройки камеры требуют написания скриптов.
Код для камеры должен быть записан в файле localScript, который размещают либо
в папку StarterPlayerScripts, либо в StarterPack.
Рассмотрим положение камеры, которая отслеживает положение игрока, сверху.
Вернем начальные характеристики камеры в исходное положение. Создадим
localScript и разместим в папку StarterPlayerScripts. Ниже представлен код, который
будет содержать этот файл.
wait(2)
-- сервис для управления временем
local RunService = game:GetService("RunService")
-- подключаемся к камере
local Camera = workspace.CurrentCamera
-- определяем локального игрока
local player = game:GetService("Players").LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
-- отслеживание положения с ориентацией
25
Camera.CameraType = Enum.CameraType.Scriptable
--RunService.Heartbeat — запуск кадра после симуляции физики
--RunService.Stepped — запуск кадра перед симуляцией физики
RunService.Stepped :Connect(function()
-- выстраиваем положение модели относительно игрока на каждом кадре
Camera.CFrame = CFrame.new(character:WaitForChild("HumanoidRootPart").
Position) +
Vector3.new(0,25,0)
Camera.CFrame *= CFrame.Angles(math.rad(-90),0,0)
end)
Если запустить проект, то мы увидим, как после загрузки камера перейдет
в режим просмотра сверху и будет следовать за персонажем (рис. 2.3).
РИС. 2.3. КАМЕРА: ВИД СВЕРХУ
СВЕТ И ЦВЕТ
В разработке игр свет и сочетание цветов играют одну из важных ролей. С помощью цвета можно передать настроение в игре и вызвать у игрока соответствующие эмоции. Цвет влияет на реакцию, концентрацию внимания, скорость
утомляемости. С помощью цвета можно также противопоставить игровых персонажей. Если мир большой или в нем используется много игровых объектов, то
для помощи игроку в ориентации и порядка действий используют определенные
контрастные цвета и подсветку.
Приведу пример одной игровой сцены, созданной в Roblox Studio с разным выбором цветовой палитры и освещения (рис. 2.4–2.6).
26
РИС. 2.4. ТЕМНЫЕ ТОНА С ПРЕОБЛАДАНИЕМ СИНЕГО
РИС. 2.5. ЯРКИЕ ТОНА: КРАСНЫЙ И ОРАНЖЕВЫЙ ЦВЕТА
27
РИС. 2.6. ЯРКИЕ ТОНА С ПРЕОБЛАДАНИЕМ ЗЕЛЕНОГО И ГОЛУБОГО ЦВЕТА
Рассматривая такие локации, можно испытать определенные эмоции и сделать
предположения о жанре этих игр: хоррор, фэнтези, творческий жанр, RPG (roleplay game, ролевая игра). На скриншотах видны игровые объекты, которые выделяются на общем фоне, тем самым вызывая интерес игрока и желание подойти
к ним.
Мы рассмотрели дневное время, где можно изменять цвет освещения и текстуру
неба. Аналогично можно работать и с ночным временем суток: сделать его сказочным, добавив элементы люминесценции, или мрачным и зловещим.
Цвет может использоваться для разграничения персонажей по их назначению
или по отношению к игроку и другим персонажам. На рисунке ниже представлены пять роботов разного цвета (рис. 2.7). Посмотри на них и определи для
себя, какие из их вызывают симпатию, а какие настороженность. Какие роботы
подошли бы на роли добрых персонажей, а какие — на злых?
Роботы одинаковы по своей конструкции, но разные по цвету. Скорее всего, большинство читателей выберут трех роботов в качестве положительных персонажей
(зеленый, синий и желтый), а оставшихся двух (красный и серый) — в качестве
отрицательных. Наш мозг сразу разделил роботов, хотя мы и не знаем, что они
на самом деле делают в игре. Можно использовать это в сюжете: либо подыграть
мозгу, либо сыграть на контрасте (серый и красный — добрые и полезные,
а остальные вредные и опасные).
28
РИС. 2.7. РАЗНОЦВЕТНЫЕ РОБОТЫ
Если твоя игра командная, где одна группа игроков играет против другой, то для
идентификации «свой-чужой» также можно использовать цвет (рис. 2.8).
РИС. 2.8. РАЗБИВАЕМСЯ НА КОМАНДЫ
Экспериментируй в своей игре с цветами. Есть очень много информации в интернете и масса книг по использованию цвета в дизайне продукта, разработке
игр и т. д. Поэтому так важно вначале придумать сюжет игры, персонажей
и механику. Понимая, как у тебя будет всё это работать, ты делаешь акценты
29
цветовым решением, освещением, звуком, всплывающими подсказками и т. д.,
чтобы помочь игроку погрузиться в игру, заинтересовать его и показать, какие
любовь и труд были в нее вложены.
АТМОСФЕРА
Под атмосферой в игре подразумеваются сразу два понятия:
формирование игрового окружения для создания эмоциональной картины
(веселое фэнтези, выживание, творческий режим, хоррор, масштабность событий);
состав воздуха, поведение лучей света в воздухе (вблизи и на горизонте),
атмосферные явления.
Первое понятие я рассмотрел выше, и, думаю, эта информация уже дала представление об атмосфере игры. Поэтому сейчас рассмотрим воздух и атмосферные
явления.
Например, туман часто используют, чтобы оградить пространство или скрыть его
либо временно, либо навсегда. Это делается ради экономии ресурсов компьютера
и экономии времени на разработку фоновых изображений или объектов. Туман
используют и для визуализации реалистичных атмосферных явлений: в горах,
над рекой или над болотом.
Воздух тоже может быть разным. В лесу или высоко в горах он может быть чистым, а в горах еще и разреженным. В городе, селах или в пустынях в воздухе
может быть пыль, выхлопы, летающие песчинки, клубы дыма и т. д. В зависимости от качества воздуха по-разному будет себя вести и освещение: где-то оно
яркое, а где-то тусклое. В каком-то случае свет будет однотонный, а в каком-то
распадаться на спектр или изменять цвет в зависимости от положения источника
света и распространения лучей в игровом мире.
Настройки атмосферы воздуха находятся во вкладке Lighting в окне Explorer.
По умолчанию при создании нового проекта в этой вкладке есть уже элемент
Atmosphere: это продвинутый вариант настройки.
Пройдемся по свойствам этого элемента:
Density — количество частиц в воздухе. От их концентрации зависит видимость
и прохождение света.
Offset — уровень детализации. Этот параметр нужно правильно сочетать
с предыдущим.
Haze — настройка тумана. В сочетании с параметром Color можно настроить
его цвет.
Glare — блики от солнца.
Decay — настройка цвета с градацией от выбранного цвета к цвету основного
освещения во вкладке Lighting и цвету тумана.
30
РИС. 2.9. НАСТРОЙКА АТМОСФЕРЫ В ИГРЕ
В старых версиях Roblox некоторые настройки были во вкладке Lighting. Их можно
включить и сейчас (настройка тумана, частиц), если удалить элемент Atmosphere
(рис. 2.10).
РИС. 2.10. НАСТРОЙКА ТУМАНА И ЧАСТИЦ АЛЬТЕРНАТИВНЫМ СПОСОБОМ
31
Ниже представлены варианты игровых сцен (локаций) с разными значениями
свойств элемента Atmosphere.
РИС. 2.11. НУЛЕВЫЕ ЗНАЧЕНИЯ DENSITY, OFFSET, HAZE И GLARE
РИС. 2.12. DENSITY = 0.5, OFFSET = 0.5
32
РИС. 2.13. HAZE = 3, OFFSET = 1, COLOR = (197, 199, 52)
РИС. 2.14. H
AZE = 3, OFFSET = 1, GLARE = 2, COLOR = (197, 199, 52), DECAY = (131, 10, 1),
DENSITY = 0.25, CLOCKTIME = 17,5
Интересные решения можно создать в мире с динамически меняющимися
временем суток и атмосферой. Ниже представлен пример скрипта смены дня
и ночи с изменением интенсивности тумана, бликами солнца и цветом рассеяния лучей.
33
-- время суток
local t = 0
-- количество частиц и интенсивность блика
game.Lighting.Atmosphere.Density = 0.3
game.Lighting.Atmosphere.Glare = 2.0
-- бесконечный цикл смены дня и ночи
while wait(0.1) do
game.Lighting.ClockTime = t
t +=0.1
if t >24 then
t = 0
-- утром и вечером туман 3, а цвет рассеяния красный
elseif (t >=0 and t< 11) or (t >=17 and t< 24) then
game.Lighting.Atmosphere.Haze = 3
game.Lighting.Atmosphere.Decay = Color3.fromRGB(202, 45, 5)
end
-- днем туман 2.5, а цвет рассеяния желтоватый
elseif t >=11 and t< 17 then
game.Lighting.Atmosphere.Haze = 2.5
game.Lighting.Atmosphere.Decay = Color3.fromRGB(202, 189, 85)
end
Скрипт можно создать в Workspace. Для ускорения времени стоит задержка
в 0.1 секунду для каждого шага в 0.1 часа.
ЗАДАНИЯ
Создай сцену у моря в ясную лунную ночь.
Используя готовые модели из окна Toolbox, собери сцену «Рассвет на опушке
леса».
Создай сцену с горными вершинами, на середине высоты которых видны облака и туман.
СБОР, РАЗРУШЕНИЕ, РЕМОНТ, ЛЕЧЕНИЕ
И НАНЕСЕНИЕ УРОНА
Теперь познакомимся с основными игровыми механиками. Почти в каждой игре
есть сбор предметов. Предмет можно собрать, и тогда он либо даст какой-то бонус
персонажу, либо финансово обогатит. А можно взять предмет и положить его
в рюкзак, чтобы использовать в дальнейшем.
Чтобы взять предмет, нужно не только подойти к нему, но и произвести действия — запустится анимация, и процесс сбора станет интерактивным.
34
СБОР
В Roblox Studio заложено множество инструментов для работы с игровыми объектами. На рисунке ниже представлены самые распространенные предметы,
которые обычно собирают в играх: ключи, оружие (бомбы, боеприпасы), еда
(аптечка), деньги (золото).
РИС. 2.15. ПРЕДМЕТЫ ДЛЯ СБОРА
Обычно форма предмета и его цвет будут говорить о его назначении, но ты всегда можешь сделать для игрока дополнительную подсказку, для чего нужны эти
предметы, или добавить обучающий квест по работе с ними. Представленные
на рисунке игровые объекты созданы в Roblox Studio. Протестируй их, открыв
файл sbor_exep.rbxl.
Самый простой способ сбора предметов — подойти к ним и коснуться, после чего
они исчезнут и сразу дадут какой-то бонус (повышение здоровья, неуязвимость,
прыгучесть и т. д.). За предметы также могут начисляться очки (фиксируется
игровой опыт).
Процесс начисления очков рассмотрим дальше, а сейчас поговорим о процессе
сбора. Описанный способ применим для монетки и яблока. У этих предметов
может быть такой скрипт:
function apple_money()
script.Parent:Destroy()
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
Игровые объекты в Roblox могут быть как цельными мешами (3D-объектами)
(Part, Union), так и сборными моделями (Model). В этом примере яблоко и монета — модели. Поэтому у каждой из них есть главная часть PrimaryPart, которая
указывается в свойствах.
35
РИС. 2.16. PRIMARYPART ДЛЯ ЯБЛОКА
Если мы касаемся PrimaryPart, то вся модель уничтожается.
Коснуться этих элементов может кто и что угодно, и скрипт сработает в любом
случае — например, если есть динамический элемент, который случайно соприкоснулся при движении с этим объектом.
Если нужно, чтобы скрипт срабатывал только при касании игроков, то можно
прописать это так:
function apple_money(player)
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent:Destroy()
end
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
Функция будет выполняться только при касании игрового персонажа с элементом Humanoid. Такой элемент есть у всех игроков, а также может быть и у NPC
(неигровых персонажей), которые при этом могут быть ограничены в действиях.
Можно использовать сбор с помощью мышки. Например, собирать предметы по
наведению курсора и клику левой кнопкой мыши на предмете.
Реализуем это для аптечки и гаечного ключа. Для начала добавим этим элементам функцию ClickDetector.
36
РИС. 2.17. ДОБАВЛЕНИЕ ИГРОВЫМ ЭЛЕМЕНТАМ ПАРАМЕТРА CLICKDETECTOR
Затем добавим Script и пропишем код:
function kit_span()
script.Parent:Destroy()
end
script.Parent.ClickDetector.MouseClick:Connect(kit_span)
Обрати внимание: срабатывание нажатия мыши на предмете может произойти
на разных расстояниях между игроком и предметом. Мышь работает хорошо
в режиме от третьего лица.
Но не всегда нужно, чтобы игрок мог захватить предмет на любом расстоянии, —
задачу усложняют, заставляя игрока подойти к предмету.
37
Реализуем это для бомбочки (боеприпаса) и добавим ей ClickDetector. В моем примере бомбочка — это модель, и, следовательно, нужно добавить этот параметр
одной из частей, по которой игрок будет щелкать (рис. 2.18).
РИС. 2.18. CLICKDETECTOR ДЛЯ ЭЛЕМЕНТА МОДЕЛИ
Чтобы клик сработал при непосредственной близости игрока к предмету, сделаем
вот что:
добавим платформу, на которую может встать игрок, — на ней будет располагаться игровой объект;
создадим логическую переменную типа BoolValue.
Создадим платформу и назовем ее activat. Для нее я добавлю script и переменную
BoolValue с именем boolActive.
Логическую переменную можно добавить как в общее рабочее пространство,
так и к определенному предмету. Чтобы не запутаться, добавим ее к предмету.
Сделать это можно с помощью знака + напротив выбранного объекта, а затем
найти нужный параметр (рис. 2.19).
38
РИС. 2.19. ДОБАВЛЕНИЕ ЛОГИЧЕСКОЙ ПЕРЕМЕННОЙ И ПЛАТФОРМЫ
РИС. 2.20. ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
Такие типы переменных хороши тем, что к ним можно получить доступ из других скриптов. Для этой желтой платформы добавим небольшой скрипт, который
будет изменять значение переменной с false на true при касании платформы.
Не забудь предварительно поставить якорь на платформу.
39
function on()
script.Parent.boolActive.Value = true
end
script.Parent.Touched:Connect(on)
Теперь перейдем к игровому объекту bomb. В моем случае это модель, поэтому
сначала нужно определить, какая часть будет PrimaryPart, а затем добавить ей
параметр ClickDetector. Самой модели добавляем script.
РИС. 2.21. ДОБАВЛЯЕМ CLICKDETECTOR В PRIMARYPART ОБЪЕКТА BOMB
Перейдем в скрипт модели и создадим функцию, которая проверяет значение
логической переменной boolActive: если она true, то модель уничтожается.
local platform = game.Workspace.activat.boolActive
function destBomb()
if platform.Value == true then
script.Parent:Destroy()
end
end
script.Parent.Union.ClickDetector.MouseClick:Connect(destBomb)
40
Теперь при тестировании мы можем щелкать по бомбе на расстоянии, а она не
будет исчезать. Она исчезнет, только если игрок встанет на желтую платформу
и кликнет по bomb.
Есть еще один вариант сбора предметов — нажатие клавиши. Обычно в играх
предлагают подобрать предмет с помощью клавиши на клавиатуре, когда мы
к нему подходим. Реализуем такой вариант в Roblox Studio, но для этого создадим
LocalScript, который разместим в папке StarterPlayerScripts в окне Explorer. Это связано с особенностью вызова событий нажатия клавиш пользователем (игроком).
РИС. 2.22. ЛОКАЛЬНЫЙ СКРИПТ ДЛЯ РАБОТЫ
С ПОЛЬЗОВАТЕЛЬСКИМ ВВОДОМ
Ниже представлен пример кода для локального скрипта.
local ContextActionService = game:GetService("ContextActionService")
local key = game.Workspace.key
local function handleAction(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
if actionName == "Key" then
print("Забрал ключ")
key:Destroy()
end
end
end
ContextActionService:BindAction("Key", handleAction, true, Enum.KeyCode.F)
Для работы с событиями вызван класс ContextActionService, который и помогает
обработать пользовательский ввод. Определение этого события и работа с ним
41
обрабатывается в функции handleAction. Функция Enum.UserInputState перечисляет события от пользовательского ввода (клик мыши, движение мыши, нажатие
и отжатие клавиши, нажатие и отжатие на сенсорном экране). В нашем случае
Enum.UserInputState.Begin — это нажатие клавиши, при котором задается имя события и действия программы.
Строка ContextActionService:BindAction — это привязка действия к пользовательскому вводу. В ней указываем, какое действие совершаем (обращаясь по имени
события), какую функцию вызываем, определяем наличие кнопки на сенсорном
экране, а также какой элемент пользовательского ввода используем. В нашем
случае это нажатие на клавишу F. Переменная key ссылается на игровой объект
«Ключ». Если мы нажмем в игре на клавишу F, то ключ исчезнет, а в окне Output
появится надпись «Забрал ключ».
При такой работе скрипта мы можем забрать ключ, находясь в любом месте
игры. Чтобы забирать предмет по нажатию клавиши в непосредственной близости от него, нужно поставить предмет-триггер. Это может быть как некоторая
невидимая область, так и конкретный предмет (например, желтая платформа,
как в случае со снарядом).
Пример сочетания триггера и нажатия клавиши рассмотрим с последним представленным игровым объектом — мечом. Это модель под названием sword, которую можно найти во вкладке Explorer.
Добавим логическую переменную BoolValue с именем ggg (рис. 2.23).
РИС. 2.23. ЛОГИЧЕСКАЯ ПЕРЕМЕННАЯ GGG
42
Вокруг меча будет невидимая или полупрозрачная область (триггер), через которую можно проходить и которая будет фиксировать процесс прохода и выхода.
Сделаем script, где будем изменять значение логической переменной.
РИС. 2.24. ТРИГГЕР ДЛЯ МЕЧА
Скрипт триггера достаточно простой:
function pod()
game.Workspace.ggg.Value = true
end
function free()
game.Workspace.ggg.Value = false
end
script.Parent.Touched:Connect(pod)
script.Parent.TouchEnded:Connect(free)
С его помощью мы изменяем значение переменной на true, когда заходим в нее,
и на false — когда выходим.
Забирать меч будем клавишей F. Но есть ряд особенностей использования одной
и той же клавиши для разных действий. В нашем случае это взятие ключа и меча
при вхождении в триггер. Поэтому нужны два ContextActionService:BindAction(),
которые критичны по обработке событий из-за порядка написания: кто последний, тот и главный. Здесь понадобится покадровый обработчик RunService. Его
задача — проверять логическую переменную и включать или отключать привязку
к действию пользователя. Включают ее с помощью BindAction(), а отключают —
с UnbindAction().
43
К имеющемуся локальному скрипту добавим новую функцию для отключения
и включения привязки. Также допишем новое действие — забирание меча при
попадании в зону триггера при нажатой клавише F.
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")
-- определяем ссылку на ключ и меч
local key = game.Workspace.key
local sword = game.Workspace.sword
-- функция по работе с событиями от пользователя
local function handleAction(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
if actionName == "Key" then
print("Забрал ключ")
key:Destroy()
elseif actionName == "Sword" and game.Workspace.ggg.Value == true then
print("Забрал меч")
sword:Destroy()
end
end
end
-- Проверяем значение логической переменной для триггера меча
function active()
if game.Workspace.ggg.Value == true then
ContextActionService:BindAction("Sword", handleAction, true, Enum.
KeyCode.F)
else
ContextActionService:UnbindAction("Sword")
end
end
-- первоочередное включение с обработкой события для ключа
ContextActionService:BindAction("Key", handleAction, true, Enum.KeyCode.F)
-- обработка каждого игрового кадра
RunService.Stepped:Connect(active)
Проверим работу скрипта. Сначала заберем монету, потом меч, а затем перезай
дем и заберем эти предметы в обратном порядке.
Документация:
ContextActionService: https://create.roblox.com/docs/reference/engine/classes/
ContextActionService.
KeyCode https://create.roblox.com/docs/reference/engine/enums/KeyCode.
RunService: https://create.roblox.com/docs/reference/engine/classes/RunService.
Есть еще один способ взаимодействия с помощью клавиатуры и геймпада — использование ProximityPrompts (подсказки при приближении). Пример — модель
двери со стенками, где к ручке двери добавлена функциональность ProximityPrompt
(рис. 2.25).
44
РИС. 2.25. ДВЕРЬ С ПОДСКАЗКОЙ ДЛЯ ОТКРЫТИЯ С ПОМОЩЬЮ КЛАВИАТУРЫ
Для этой функции в окне Properties можно указать расстояние, на котором активируется подсказка MaxActivationDistance, клавиша для активации KeyboardKeyCode или
GamepadKeyCode, текст подсказки ObjectText, время активации HoldDuration (рис. 2.26).
РИС. 2.26. СВОЙСТВА PROXIMITYPROMPT
45
Если подойти к двери на расстоянии меньше 10 studs (stud — единица измерения
линейных размеров в Roblox), появится подсказка для игрока о том, что нужно
нажать указанную клавишу (рис. 2.27).
РИС. 2.27. ПОДСКАЗКА ДЛЯ ДЕЙСТВИЯ
В качестве примера приведен вариант исчезновения двери с ручкой. С помощью
команд Transparency и CanCollide я реализую задуманное.
Скрипт выполнения можно создать как непосредственно для прикрепленной
функции ProximityPrompt, так и в общей папке для работы с клиентом и сервером
ServerScriptService.
Я использую второе решение: код сконструирован так, чтобы в него можно
было добавлять другие объекты, с которыми возможна активация в дальнейшем. С подсказкой мы можем взаимодействовать тремя разными способами:
полная реализации активации по истечении времени, после начала активации
или по завершении активации пользователем. Код, представленный ниже,
имеет общий характер.
local ProximityPromptService = game:GetService("ProximityPromptService")
local function onPromptTriggered(promptObject, player)
print("Открыл дверь")
print(promptObject.Parent)
-- исчезание двери и ручки
if promptObject.Parent.Name =="doorknob" then
promptObject.Parent.Parent.door.Transparency = 1
promptObject.Parent.Transparency = 1
prlocal functilocal function onPromptHoldEnded(promptObject, player)
--print("Открыл дверь")
--print(promptObject.Parent)
46
end
-- Connect prompt events to handling functions
ProximityPromptService.PromptTriggered:Connect(onPromptTriggered)
ProximityPromptService.PromptButtonHoldBegan:Connect(onPromptHoldBegan)
ProximityPromptService.PromptButtonHoldEnded:Connect(onPromptHoldEnded)
Все события работы с подсказками обрабатываются в ProximityPromptService.
Документация по этой функциональности: https://create.roblox.com/docs/tutorials/
building/ui/proximity-prompts.
В представленном коде дверь и ручка исчезнут по завершении времени акти
вации.
ЛЕЧЕНИЕ И ПОВРЕЖДЕНИЕ
Следующий элемент в играх — нанесение урона и восстановление сил. В сложных
играх разработчики создают балансировку по урону и восстановлению, чтобы
усреднить показатели и сделать игру не слишком сложной, но и не слишком
легкой.
В качестве примера получения игроком повреждений рассмотрим касание опасной области, например лавы.
Создадим блок Part c размерами (15, 1, 10) и наложим текстуру лавы на верхнюю
грань. Текстуру можно взять из Toolbox. Назовем область Lava и вставим скрипт
(рис. 2.28).
РИС. 2.28. ОБЛАСТЬ УРОНА И ПОВРЕЖДЕНИЙ
Так как игрок будет получать повреждения от касания, то алгоритм действий
возьмем из скрипта с яблоком, но с некоторыми дополнениями. Изменим значения параметра Humanoid, отвечающего за жизненные силы игрока Health.
47
-- нанесение урона игроку
function damage(player)
if player.Parent:FindFirstChild("Humanoid") then
player.Parent.Humanoid.Health -=1
-- альтернативный вариант урона
-- player.Parent.Humanoid:TakeDamage(1)
end
end
script.Parent.Touched:Connect(damage)
В пятой строчке используется альтернативный вариант нанесения урона. По такому
же принципу можно написать алгоритм для восстановления здоровья персонажа
(рис. 2.29). Ниже представлен скрипт для аптечки, которую игрок собирает по
клику левой кнопки мыши. В его основе скрипт с аптечкой, но с дополнениями.
РИС. 2.29. УРОН И ВОССТАНОВЛЕНИЕ
48
local chat = game:GetService("Chat")
local bot = game.Workspace.out
function kit_span(player)
chat:Chat(bot,"Забрал аптечку кликом мыши")
script.Parent:Destroy()
-- выводим имя игрового персонажа,
-- кликнувшего по аптечке
print(player)
-- увеличиваем здоровье на 50
player.Character.Humanoid.Health +=50
end
script.Parent.ClickDetector.MouseClick:Connect(kit_span)
Такой алгоритм лечения и повреждения можно использовать на объектах
с элементом Humanoid. Например, создать своего игрового персонажа (либо
NPC, бота, робота, неодушевленный предмет) и добавить этот параметр, как
показано на рис. 2.30. Сверху появится объект, показывающий количество
здоровья. При клике по нему здоровье Humanoid уменьшается, как и размеры
красного индикатора.
РИС. 2.30. БОТ В ВИДЕ БЛОКА
Описанные игровые механики располагаются в файле dumage_health.rbxl. Ты можешь поэкспериментировать: создать все с нуля или воспользоваться готовым
решением.
49
РАЗРУШЕНИЕ И РЕМОНТ
Кроме лечения и нанесения повреждений игрокам и NPC также бывают разрушения и поломки зданий, техники, хрупких предметов. Иногда разрушенные
игровые элементы можно ремонтировать или восстанавливать. Например, чинить
автомобиль после аварий, восстанавливать здания после нападения противника,
склеивать разорванные фотографии в головоломках и т. д.
В файле dumage_health.rbxl есть примеры реализации разрушения и восстановления. В качестве примера разрушения и повреждения возьмем модель avtomobile.
В этой модели детали приварены Weld или закреплены на поворотных шарнирах
Hinge и Cylindrical.
Простые варианты создания моделей я привел в книге «Roblox. Играй, программируй и создавай свои миры». В этой книге в разделе «Разработка сложных структур игровых объектов и локаций» я приведу более углубленную информацию по
созданию моделей и структур.
На рис. 2.31 представлена модель. Повреждения она будет получать в результате
краш-теста, то есть при проверке степени безопасности и повреждений машины
при столкновениях. Двери, багажник и капот закреплены на подвижных петлях
и могут отскакивать от удара в зависимости от его силы. На капоте автомобиля
есть два скрытых эффекта: Smoke и Fire. Они активируются при ударе о стену.
Стена находится в невидимой области Stena.
РИС. 2.31. МОДЕЛЬ АВТОМОБИЛЯ ДЛЯ КРАШ-ТЕСТА
Слева от автомобиля есть синий игровой объект с разноцветными кнопками.
Красная и зеленая кнопки нужны для настройки скорости движения автомобиля — красная уменьшает скорость на 50 и до 0, а зеленая увеличивает на 50.
Желтая кнопка нужна для запуска автомобиля. Чтобы вернуть автомобиль в исходную точку и задать новую скорость, используем коричневую кнопку. У всех
кнопок есть свойство ClickDetector.
50
При взаимодействии с кнопками появляется чат, отображающий, что делает
игрок. В модели avtomobile есть скрипт для отслеживания столкновения.
РИС. 2.32. СТРУКТУРА МОДЕЛИ AVTOMOBILE
Ниже представлена структура кода:
local
local
local
local
Fire = script.Parent.Wedge.Fire
Smoke = script.Parent.Wedge.Smoke
Kapot = script.Parent.Kapot.HingeConstraint
Bagash = script.Parent.Bagash.HingeConstraint
-- выключение огня и дыма
Fire.Enabled = false
Smoke.Enabled = false
-- положение капота и багажника в норме
Kapot.LowerAngle = 0
Bagash.LowerAngle = 0
-- функция включения огня и дыма
-- отображение поломки в багажнике и капоте
function avaria(box)
if box.Name =="Stena" then
Fire.Enabled = true
Smoke.Enabled = true
Kapot.LowerAngle = -50
Bagash.LowerAngle = 45
script.Parent.car.LinearVelocity.VectorVelocity =Vector3.zero
end
end
-- запуск функции при столкновении со "Stena"
script.Parent.PrimaryPart.Touched:Connect(avaria)
51
У пульта управления тоже есть скрипт для кнопок. В скрипте все измененные
значения скорости хранятся в глобальной целочисленной переменной Speed.
РИС. 2.33. ГЛОБАЛЬНАЯ ПЕРЕМЕННАЯ SPEED
local chat = game:GetService("Chat")
-- кнопки
local Max = script.Parent.Max.ClickDetector
local Min = script.Parent.Min.ClickDetector
local Run = script.Parent.Run.ClickDetector
local Sbros = script.Parent.Sbros.ClickDetector
-- исходное положение автомобиля
local PosIDE = game.Workspace.avtomobile.PrimaryPart.Position
-- функция увеличения скорости
function MaxAct()
script.Parent.Speed.Value-=50
chat:Chat(script.Parent.Union, tostring(script.Parent.Speed.Value))
end
-- функция уменьшения скорости
function MinAct()
script.Parent.Speed.Value+=50
if script.Parent.Speed.Value>0 then
script.Parent.Speed.Value = 0
end
chat:Chat(script.Parent.Union, tostring(script.Parent.Speed.Value))
end
-- функция передачи значения скорости в LinearVelocity автомобиля
function RunAct()
chat:Chat(script.Parent.Union, "Запуск теста")
wait(1)
game.Workspace.avtomobile.car.LinearVelocity.VectorVelocity = script.Parent.
Speed.Value*Vector3.zAxis
end
52
-- функция возврата автомобиля в исходную точку
function SbrosAct()
chat:Chat(script.Parent.Union, "Исходная позиция")
wait(1)
game.Workspace.avtomobile:PivotTo(CFrame.new(PosIDE))
end
if game.Workspace.avtomobile.PrimaryPart.Position.Z>= -16 then
game.Workspace.avtomobile.car.LinearVelocity.VectorVelocity=Vector3.zero
end
Max.MouseClick:Connect(MaxAct)
Min.MouseClick:Connect(MinAct)
Run.MouseClick:Connect(RunAct)
Sbros.MouseClick:Connect(SbrosAct)
Ниже представлен результат краш-теста при скорости в –200 (знак «минус» отвечает только за направление вектора скорости) (рис. 2.34).
Процесс разрушения автомобиля можно усовершенствовать. Например, сделать
так, чтобы отваливались двери и колеса. Если вернуть машину в исходное положение, то она будет продолжать гореть, а части будут раскрыты.
РИС. 2.34. КРАШ-ТЕСТ АВТОМОБИЛЯ
Реализуем его починку: сделаем так, что починить машину игрок может, если
взял гаечный ключ. И этот гаечный ключ будет отображаться как инструмент.
Чтобы использовать игровой объект в качестве инструмента, нужно разместить
его в элемент Tool. Чтобы игрок мог брать инструмент в руки, нужно привязать
к нему объект с именем Handle (рис. 2.35).
53
РИС. 2.35. ГАЕЧНЫЙ КЛЮЧ
Если все сделано правильно, то при тестировании инструмент попадет в руки
персонажа, а ниже на экране отобразится ячейка рюкзака (рис. 2.36).
РИС. 2.36. БЕРЕМ ИНСТРУМЕНТ В РУКИ
54
Чтобы работа инструментом выглядела динамично, добавим анимацию. В примере ниже — моя анимация для молотка и кирки, но она может подойти и для
гаечного ключа (рис. 2.37).
РИС. 2.37. АНИМАЦИЯ ДЛЯ РАБОТЫ С ИНСТРУМЕНТОМ
Из рис. 2.33 видно, что для инструмента добавлен локальный скрипт. Он нужен
для запуска анимации, когда инструмент оказывается в руках персонажа. Ниже
представлен скрипт:
function remont()
local human = script.Parent.Parent:FindFirstChild("Humanoid")
local anim = game.ReplicatedStorage.Animation
if human then
local action = human:LoadAnimation(anim)
action: Play()
wait(1)
end
end
script.Parent.Activated:Connect(remont)
Анимация содержится в ReplicatedStorage. Это связано с тем, что мы вызываем
анимацию для конкретного игрока, и она может быть применена ко всем игрокам, которые воспользуются этим инструментом.
Ремонт автомобиля будет завязан на подсчете касаний инструментом. Касание
будем определять по имени, и в нашем случае имя — spanner (рис. 2.33). Допустим, что при 10 касаниях машина полностью восстановится. Это значит, что
прекратятся возгорание и дым, а багажник и капот примут исходное положение,
как и двери. За подсчет количества касаний отвечает целочисленная переменная
NewOld, которая будет в модели автомобиля.
55
Осталось добавить в скрипт для автомобиля нужные строки по выключению огня,
дыма и возвращению в исходное положение дверей на петлях. Сделаем это не
сразу, а исправим постепенно в зависимости от количества ударов.
В скрипте укажем 4 шага: первый — выключение дыма, второй — выключение
огня, третий — восстановление багажника и задних дверей и четвертый — восстановление капота и передних дверей. Ниже представлен весь код скрипта для
модели автомобиля.
local
local
local
local
Fire = script.Parent.Wedge.Fire
Smoke = script.Parent.Wedge.Smoke
Kapot = script.Parent.Kapot.HingeConstraint
Bagash = script.Parent.Bagash.HingeConstraint
local chat = game:GetService("Chat")
-- выключение огня и дыма
Fire.Enabled = false
Smoke.Enabled = false
-- положение капота и багажника в норме
Kapot.LowerAngle = 0
Bagash.LowerAngle = 0
-- функция включения огня и дыма
-- отображение поломки в багажнике и капоте
-- ремонт автомобиля
function avaria(box)
print(box)
-- касание стены
if box.Name =="Stena" then
Fire.Enabled = true
Smoke.Enabled = true
Kapot.LowerAngle = -50
Bagash.LowerAngle = 45
script.Parent.car.LinearVelocity.VectorVelocity =Vector3.zero
chat:Chat(script.Parent.PrimaryPart, "Максимальное повреждение ")
script.Parent.NewOld.Value=0
-- касание инструментом
elseif box.Name =="spanner" then
script.Parent.NewOld.Value+=1
end
-- ремонт автомобиля
if ((script.Parent.NewOld.Value>=2 and script.Parent.NewOld.Value<4)
and script.Parent.NewOld.Value ~=0) then
Smoke.Enabled = false
chat:Chat(script.Parent.PrimaryPart, "Восстановлено 25%")
end
if
end
if
(script.Parent.NewOld.Value>=4 and script.Parent.NewOld.Value<6) then
Fire.Enabled = false
chat:Chat(script.Parent.PrimaryPart, "Восстановлено 50%")
(script.Parent.NewOld.Value>=6 and script.Parent.NewOld.Value<8) then
Kapot.LowerAngle = 0
chat:Chat(script.Parent.PrimaryPart, "Восстановлено 75%")
56
end
if
script.Parent.dverLB.dverLB.HingeConstraint.LowerAngle =0
script.Parent.dverRB.dverRB.HingeConstraint.LowerAngle =0
(script.Parent.NewOld.Value>=8 and script.Parent.NewOld.Value<10) then
Bagash.LowerAngle = 0
chat:Chat(script.Parent.PrimaryPart, "Восстановлено 100%")
script.Parent.dverLFF.dverLF.HingeConstraint.LowerAngle =0
script.Parent.dverRFF.dverRF.HingeConstraint.LowerAngle =0
end
end
-- запуск функции при столкновении со "Stena"
script.Parent.PrimaryPart.Touched:Connect(avaria)
Над автомобилем будет выводиться сообщение о степени повреждения и восстановления (рис. 2.38).
РИС. 2.38. ПОВРЕЖДЕНИЕ И РЕМОНТ АВТОМОБИЛЯ
57
Другой способ восстановления и повреждения можно задать через ModuleScript
и заранее заготовленные модели разной степени повреждения, которые можно разместить в ReplicatedStorage или ServerStorage. Затем в зависимости от значения переменной или других факторов вызывать их. Этот вариант мы рассмотрим дальше.
Подробно о создании инструментов, анимации и привязке предметов к игроку
я рассказал в книге «Roblox. Играй, программируй и создавай свои миры».
ЗАДАНИЯ
Создай ключ, с помощью которого можно открыть созданную тобой дверь.
Создай полосу препятствий с элементами, которые будут наносить повреждение персонажу при непосредственном касании.
ДВИЖЕНИЕ, АНИМАЦИЯ, СИМУЛЯЦИЯ
(ФИЗИКА И ЭФФЕКТЫ)
Анимация — это способ передачи динамики повествования, демонстрация действия как игрока, так и игрового мира. Движение — это тоже анимация. Она
может быть как заранее записанным действием, так и продуктом вычисления во
время действий сторонних факторов: нажатия кнопки, столкновения, симуляции
физических явлений и т. д.
В предыдущей книге я рассказывал, как записать анимацию для персонажа или
NPC гуманоидного вида. Здесь же приведу общий обзор. Под анимацией можно
понимать движение автомобиля, падение листвы, движение качелей, полет бабочки или птицы, танцы и ходьбу игрового персонажа и т. д.
Для начала рассмотрим заранее записанные анимации, которые создаются благодаря записям изменений свойств по кадрам. К таким свойствам могут относиться:
положение, цвет, размер, физические параметры (скорость, сила, упругость,
прозрачность и т. д.). В Roblox Studio есть инструмент записи анимации гуманоидов, поэтому сделать анимацию для игрока или NPC не составит труда. Для
этого перейди во вкладку главного меню AVATAR и выбери Rig Build. Этот плагин
позволит выбрать заготовку для гуманоида с настроенными ригами частей тела
(риг — набор управляемых элементов для анимации). Нужно только выбрать
подходящий вариант модели. Рекомендую использовать Mesh Rig или Block Rig
независимо от выбранного формата — R15 или R6 (рис. 2.39). Документация
рекомендует использовать R15.
В примере ниже представлен Mesh Rig R15. Для создания анимации нужно дождаться загрузки выбранной модели (рис. 2.40), затем перейти во вкладку AVATAR
и выбрать Animation Editor. После этого найти загруженную модель в окне Explorer,
как правило, она имеет имя Dummy. Активируется окно редактора анимации, где
тебе предложат назвать ее.
58
РИС. 2.39. ВЫБИРАЕМ BUILD RIG
РИС. 2.40. ПОДГОТОВКА К АНИМАЦИИ ПЕРСОНАЖА
59
Итак:
1. Загруженная модель Mesh Rig.
2. Открытие Animation Editor.
3. Выбор загруженной модели в окне Explorer.
4. Активация окна редактора с возможностью создания файла анимации.
Теперь приступим к написанию анимации персонажа. Для этого определим длину
анимации в секундах, а после выберем определенные части тела модели и изменим их положение в определенный момент времени. Чтобы выбрать, в какой
момент времени какая часть модели должна двигаться, левой кнопкой мыши
щелкни по временной дорожке редактора анимации у определенного значения
времени и начни смещение элемента персонажа (рис. 2.41).
РИС. 2.41. ЗАПИСЫВАЕМ АНИМАЦИЮ ВЫБРАННЫХ ЭЛЕМЕНТОВ МОДЕЛИ
При перемещении элемента модели автоматически ставятся ключи кадра (ромбики), то есть запись положения. Если начать изменять эту часть тела в редакторе
в другое время, то кадры также запишутся, и программа настроит связь между
предыдущим и последующим кадром. Как только ты закончишь создание анимации, то проверь ее, щелкнув по треугольнику вверху редактора анимации или
нажав клавишу «Пробел».
Когда запись анимации закончена, ее нужно опубликовать, чтобы в дальнейшем
вызвать в игре (рис. 2.42). Эта анимация будет храниться на сервере и позволит
использовать ее в других проектах и готовых играх.
60
РИС. 2.42. ПУБЛИКАЦИЯ АНИМАЦИИ
При публикации тебе предложат указать название анимации, дать небольшое
описание и прикрепить иконку (рис. 2.43). Когда все будет готово, нажми кнопку
Submit.
РИС. 2.43. ОФОРМЛЕНИЕ ЗАГРУЖАЕМОЙ АНИМАЦИИ В ОБЛАКО ROBLOX
Как только процесс публикации закончится (убедись, что у тебя есть доступ в интернет), тебе предоставят ссылку на нее. Также ее можно будет найти на своей
страничке в Roblox в окне Create в разделе Animations.
61
РИС. 2.44. РЕГИСТРАЦИЯ АНИМАЦИИ
РИС. 2.45. СОЗДАННЫЕ АНИМАЦИИ НА СТРАНИЦЕ CREATE
Если выбрать анимацию на странице Create, то тебя перебросит на страницу с ее
описанием. Здесь же ты увидишь номер ID для поиска и добавления анимации
в проект Roblox (рис. 2.46).
62
РИС. 2.46. ID АНИМАЦИИ НА ЕЕ СТРАНИЦЕ
Чтобы применить эту анимацию на нашем игровом персонаже, сделай два шага.
Первый: создай элемент Animation. Сделать это можно либо в папке ReplicatedStorage
или ServerStorage, либо добавить к скрипту. Второй: создай скрипт. Для игрового
персонажа может подойти локальный скрипт в папке StarterPlayerScripts.
На рис. 2.47 показан процесс добавления анимации параметру Animation по ID.
РИС. 2.47. ДОБАВЛЯЕМ АНИМАЦИЮ ПО ID
63
В LocalScript для игрока пропишем простой код:
-- находим анимацию
local animBot = game.ReplicatedStorage.Animation
-- определяем локального игрового персонажа
local player = game:GetService("Players").LocalPlayer
wait(3)
-- находим элемент "Humanoid"
local hum = player.Character:WaitForChild("Humanoid")
wait(5)
-- Передаем структуру анимации на найденный "Humanoid" игрока
local open_anim = hum:LoadAnimation(animBot)
-- запуск анимации
open_anim:Play()
РИС. 2.48. РАСПОЛОЖЕНИЕ ЛОКАЛЬНОГО СКРИПТА
Если запустить игровой проект с загрузкой игрового персонажа, то через несколько секунд он начнет совершать те же движения, что были записаны в анимации.
Как мы убедились, создание анимации для персонажей-гуманоидов — простое
дело. Но кроме них есть и множество игровых элементов, которые могут быть
анимированы. И для этого есть два способа:
1. Применение Animation Editor с добавлением элементов Humanoid и Motor6D.
2. Запись анимации с помощью скрипта.
Рассмотрим оба подхода. Все описанные в этом разделе примеры ты найдешь
в файле animation_move.rbxl.
В первом случае создадим модель — железнодорожный шлагбаум с именем barier
и качели с именем seesaw. При сборке модели установим все элементы в состоянии покоя, то есть так, как они располагались бы в реальном мире без движения.
На рис. 2.49 представлена модель шлагбаума.
64
РИС. 2.49. МОДЕЛЬ ШЛАГБАУМА
Модель состоит из трех элементов: столба, заградительного элемента и штыря.
Модели добавлен элемент Humanoid. Чтобы редактор анимации разрешил создать анимацию, нужно добавить хотя бы один Rig. В его роли выступает элемент
Motor6D (рис. 2.50).
РИС. 2.50. НАСТРОЙКА RIG
Для корректной работы двух частей их нужно связать, то есть создать иерархию
между частями. Добавим штырю элемент Motor6D — он выступает в роли главного
элемента. Также добавляем его к параметру Part0. К параметру Part1 добавляем
связанный элемент Union.
Теперь переходим в AVATAR, открываем Animation Editor, выбираем модели
и даем название анимации. Для записи анимации нам станет доступен Union
(рис. 2.51).
65
РИС. 2.51. АНИМАЦИЯ UNION
В этом примере я создал анимацию на 5 секунд, чтобы у персонажей было время
пройти мимо шлагбаума. После завершения записи переходим к публикации
и запоминанию ID. Добавим в нашу модель скрипт, а к нему элемент Animation
(рис. 2.52).
РИС. 2.52. ДОБАВЛЕНИЕ СКРИПТА И АНИМАЦИИ
66
Для элемента Animation добавляем ID нашей анимации. Скрипт будет выглядеть
так:
local player = script.Parent:WaitForChild("Humanoid")
local anim = player:LoadAnimation(script:FindFirstChildOfClass("Animation"))
anim.Looped = true
anim:Play()
Код достаточно прост: в нем мы определяем элемент Humanoid и добавляем ему
анимацию, а чтобы анимация работала непрерывно, зацикливаем ее. Подобный
вариант реализован и на модели качелей (рис. 2.53).
РИС. 2.53. КАЧЕЛИ
В качестве основного элемента используется Union из элементов формы X, а подвижной части — коромыслу — присвоен Part1. Для качелей добавлены элементы
Seat, чтобы персонаж смог сесть и покататься. При взаимодействии персонажа
с анимированным предметом он не изменяет положение и свою анимацию, так
как это строго записанный процесс. Скрипт у качелей такой же, но анимация
другая, она запускается сразу при загрузке игры.
Теперь рассмотрим анимацию предмета, которая начинается спустя время или
при воздействии пользователя. Для этого я свяжу логику железнодорожного
шлагбаума со светофором. Светофор — это тоже игровой объект, у него есть три
лампы: зеленая, красная и желтая, которые поочередно загораются (рис. 2.54).
Анимацию для этого процесса создадим в виде скрипта.
Светофор — модель в Roblox, в примере у нее название stoplight. Три лампы, созданные из цилиндров, имеют названия Green, Yellow и Red. С помощью скрипта
я буду изменять материал этих ламп с Plastic на Neon, и смена материалов будет
проходить каждые 5 секунд, то есть такое же время, как в анимации шлагбаума.
Скрипт расположим внутри светофора, а скрипт в шлагбауме можем удалить
или закомментировать.
67
РИС. 2.54. СВЕТОФОР
Чтобы шлагбаум реагировал на изменения в светофоре, добавим глобальную
логическую переменную внутрь модели и назовем ее Open. Как только загорится
зеленый свет, переменная Open примет значение true, а в остальных случаях будет
иметь значение false. Ниже приведен код описанного процесса:
-- лампы светофора
local green = script.Parent.Green
local yellow = script.Parent.Yellow
local red = script.Parent.Red
-- логическая переменная
local openAnim = script.Parent.Open
-- счетчик для светофора
local n = 1
-- цикл с шагом в 5 секунд
while wait(5) do
-- зажигаем зеленый свет
if n == 1 then
green.Material = Enum.Material.Neon
yellow.Material = Enum.Material.Plastic
red.Material = Enum.Material.Plastic
openAnim.Value =true
-- зажигаем желтый свет
elseif n ==2 then
green.Material = Enum.Material.Plastic
yellow.Material = Enum.Material.Neon
red.Material = Enum.Material.Plastic
openAnim.Value =false
-- зажигаем красный свет
elseif n ==3 then
green.Material = Enum.Material.Plastic
yellow.Material = Enum.Material.Plastic
red.Material = Enum.Material.Neon
68
openAnim.Value =false
-- обнуляем счетчик
else
n=0
end
-- изменяем счетчик
n+=1
end
При запуске мы увидим, как лампы светофора загораются каждые 5 секунд. Теперь добавим код для запуска анимации шлагбаума. Для этого добавим к скрипту
элемент Animation и укажем в нем ID анимации шлагбаума (рис. 2.55).
РИС. 2.55. АНИМАЦИЯ ШЛАГБАУМА В СКРИПТЕ ДЛЯ СВЕТОФОРА
В начало скрипта добавим две строчки:
local player = game.Workspace.barier:WaitForChild("Humanoid")
local anim = player:LoadAnimation(script:FindFirstChildOfClass("Animation"))
Тебе уже знаком этот код, но здесь есть изменения. Так как код лежит в светофоре,
а нам нужно получить доступ к Humanoid шлагбаума, то мы прописываем в первой
строчке путь к нему. Вторая строка добавляет анимацию, которая лежит в скрипте
нашего шлагбаума. Теперь осталось запустить ее в нужный момент, а именно
когда в цикле будет n==1. В этом случае вставим такой код:
anim:Play()
Если запустить игровой проект, то анимация шлагбаума будет включаться только
при загорании зеленого света. Можем усложнить алгоритм, например, позволить
игроку самому открывать шлагбаум, чтобы пройти. Для этого добавим подсказку
для активации нужной клавиши ProximityPrompt. Добавим этот элемент столбу, на
котором держится шлагбаум, и укажем его свойства, как на рис. 2.56.
69
РИС. 2.56. НАСТРОЙКА ПОДСКАЗКИ
Добавим в скрипт возможность запустить анимацию по истечении активации
нажатием клавиши E. Вот полный код:
local player = game.Workspace.barier:WaitForChild("Humanoid")
local anim = player:LoadAnimation(script:FindFirstChildOfClass("Animation"))
-- обработка событий нажатия клавиши
local Proximity = game:GetService("ProximityPromptService")
function open_barier()
anim:Play()
end
Proximity.PromptTriggerEnded:Connect(open_barier)
-- лампы светофора
local green = script.Parent.Green
local yellow = script.Parent.Yellow
local red = script.Parent.Red
-- логическая переменная
local openAnim = script.Parent.Open
-- счетчик для светофора
local n = 1
-- цикл с шагом в 5 секунд
while wait(5) do
-- зажигаем зеленый свет
if n == 1 then
green.Material = Enum.Material.Neon
yellow.Material = Enum.Material.Plastic
red.Material = Enum.Material.Plastic
70
openAnim.Value = true
anim:Play()
-- зажигаем желтый свет
elseif n == 2 then
green.Material = Enum.Material.Plastic
yellow.Material = Enum.Material.Neon
red.Material = Enum.Material.Plastic
openAnim.Value = false
-- зажигаем красный свет
elseif n == 3 then
green.Material = Enum.Material.Plastic
yellow.Material = Enum.Material.Plastic
red.Material = Enum.Material.Neon
openAnim.Value = false
-- обнуляем счетчик
else
n=0
end
-- изменяем счетчик
n+=1
end
РИС. 2.57. АКТИВАЦИЯ ОТКРЫТИЯ ШЛАГБАУМА ПО НАЖАТИИ КЛАВИШИ E
В этих примерах анимация реализована двумя способами: через запись и в скрипте. Для сравнения предлагаю создать копию шлагбаума, но без элементов
Humanoid и Motor6D, в которой будет скрипт, запускающий запрограммированную
анимацию открытия. Назову копию barier2.
Для корректной работы скрипта произведем предварительные настройки. Вопервых, переместим центр масс (опорную точку) подвижной части шлагбаума.
Во-вторых, поставим на ней якорь (Anchor), чтобы она не падала. Опорная точка
в настройках обозначается Pivot. Требуется сместить точку к краю подвижной
части, где происходит крепление со столбом (рис. 2.58).
71
РИС. 2.58. СМЕЩАЕМ PIVOT ДЛЯ КОРРЕКТИРОВКИ ДВИЖЕНИЯ
Для поворота элемента Union шлагбаума используем CFrame. Это тип данных,
который хранит координаты положения и ориентацию в пространстве в виде
матрицы. Ссылка на документацию: https://create.roblox.com/docs/reference/engine/
datatypes/CFrame.
Сначала получим CFrame опорной точки, а затем эту матрицу значений умножим
на матрицу поворота.
Матрица — это математическое понятие, которое обычно изучается на курсе
высшей математики. Матрицы удобны для работы с большим набором параметров. Согласно документации, положение и ориентация содержат двенадцать
параметров:
.
Параметры X, Y, Z — это координаты положения, а первые три строки матрицы —
параметры вращения относительно осей X, Y и Z.
Ниже представлен код по управлению шлагбаумом:
-- получаем значения CFrame точки Pivot
local Pivot = script.Parent.Union:GetPivot()
-- зацикленное движение
while wait(0.1) do
if game.Workspace.stoplight.Open.Value == true then
for i = 0, 80 do
script.Parent.Union:PivotTo(Pivot * CFrame.Angles(math.rad(i), 0, 0))
wait(0.01)
end
72
for i = 80, 0, -1 do
script.Parent.Union:PivotTo(Pivot * CFrame.Angles(math.rad(i), 0, 0))
wait(0.01)
end
end
end
В коде используется функция PivotTo, задача которой переместить объект, отсчитав положение от точки опоры. Запись CFrame.Angles отвечает за указание
угла поворота по осям. Угол поворота измеряется в радианах, поэтому используется math.rad. Из цикла видно, что угол поворота находится в диапазоне [0°,
80°]. Время задержки подобрано так, чтобы синхронизироваться с работой
первого шлагбаума.
На этом закончим с алгоритмами создания анимации и плавно перейдем к движению.
Движение — это частный случай анимации, его можно ограничивать или не
ограничивать. Заставить игровой объект двигаться можно разными способами,
один из которых мы рассмотрели выше. Но есть задачи, когда движение должно
быть спонтанным или достаточно долгим, то есть должны изменяться параметры скорости, ускорения, траектории (направления векторов движения) и т. д.
Для таких процессов разработаны функции LinearVelocity, LineForce, AngularVelocity,
AlignPosition, AlignOrientation, Torque, VectorForce.
В рамках этой книги подробно рассмотреть все инструменты физики не получится. Приведу примеры использования лишь трех функций: AlignPosition,
AlignOrientation и LinearVelocity.
По каждой их них есть документация, которую легко найти. Чтобы показать работу этих функций, я создал две модели тепловоза и железную дорогу (рис. 2.59).
РИС. 2.59. МОДЕЛИ ТЕПЛОВОЗОВ И ЖЕЛЕЗНАЯ ДОРОГА
73
Зеленый тепловоз назван train0, а синий — train1. Рассмотрим train0 (рис. 2.60).
РИС. 2.60. СТРУКТУРА МОДЕЛИ ТЕПЛОВОЗА
Ведущая часть модели — это Platf, она же указана как PrimaryPart. Для Platf добавлены AlignPosition и AlignOrientation. Добавлять их стоит через верхнее меню Create,
так как в этом случае тебе сразу же предложат указать две точки приложения
Attachment. Эти точки указывают место приложения сил.
Функция AlignPosition позволяет держать положение тела в пространстве в указанной точке. Если точка тела и указанная точка расположены на расстоянии, то тело
начнет движение ко второй точке. Поэтому важно указать Attachment0 — точку
тела движения и Attachment1 — точку назначения. Точку назначения я установил
на объекте B, который расположен по направлению движения тепловоза в туннеле (рис. 2.61).
РИС. 2.61. ОБЪЕКТ B
74
Чтобы тепловоз возвращался в исходную точку после достижения центра тела B,
я добавил объект с именем A. Его координаты модель принимает по достижении тела B. Для красоты эти точки лежат в противоположных туннелях, чтобы
создавался эффект непрерывного движения поездов. Тела A и B имеют Anchor =
true и CanCollide = false. Тем самым точки не меняют своего положения даже при
столкновении и не мешают движению тепловоза.
Все части тепловоза соединены с помощью Weld. Чтобы тепловоз начал движение, нужно отрегулировать параметры сил. В Roblox размеры игровых объектов
и материал играют важную роль, так как от них зависит также итоговая масса.
Сила, которая способна поднять тело, должна быть сравнима с силой тяжести.
Чтобы наш тепловоз не мотало, нужно использовать AlignOrientation. Эта функция
позволяет выровнять тело относительно выбранного, к которому приложена
точка Attachment1. В нашем случае она приложена к поверхности земли Baseplate.
Точка Attachment0 также прикладывается к движущемуся телу, здесь — к Platf.
На рис. 2.62 указаны настройки этих параметров для функций AlignPosition
и AlignOrientation.
РИС. 2.62. НАСТРОЙКИ ПАРАМЕТРОВ ФУНКЦИЙ ДВИЖЕНИЯ
У функции AlignOrientation напротив параметра RigityEnabled стоит галочка. Она
нужна, чтобы тепловоз быстрее реагировал на изменения смещения и мог выровняться.
Если ты сделаешь свои модели самостоятельно и проверишь эти настройки, то
результатом будет движение игрового тела к центру тела B. Обрати внимание,
что в зависимости от созданной модели значение MaxForce в AlignPosition может
быть разное.
Теперь перейдем к написанию скрипта, с помощью которого тепловоз вернется
в исходную точку A. Скрипт разместим внутри модели train0. Переброска будет
завязана на вычислении расстояния между тепловозом и точкой B и режиме работы светофора. Если будет гореть зеленый свет, то переброски не будет.
75
local postA = game.Workspace.A
local postB = game.Workspace.B
local Pivot = postA:GetPivot()
while wait(0.1) do
-- вычисляем расстояние от тепловоза до тела B
local dist =((postB.Position — script.Parent.PrimaryPart.Position).Magnitude)
-- переброска тепловоза в точку A
if dist < 31 and game.Workspace.stoplight.Open.Value == false then
script.Parent.PrimaryPart:PivotTo(Pivot)
end
end
print(dist)
Значение расстояния — 31 studs — взято на основе эксперимента с движением
тепловоза. Если создаешь свою модель, то сначала вычисли, на какое минимальное расстояние приближается твоя модель к точке B.
Теперь рассмотрим синий тепловоз train1. Его главной деталью также выступает параметр Platf, который и является PrimaryPart. К Platf приложена функция
LinearVelocity, которая задает линейную скорость (рис. 2.63). Этот параметр позволяет двигаться телу равномерно и прямолинейно. Для нее указывается точка
приложения Attachment0, в примере она приложена к Platf. Для этой функции есть
два параметра, которые нужно указать: значение скорости в векторном формате
VectorVelocity и сила воздействия на тело MaxForce, чтобы игнорировать трение
и силу тяжести.
РИС. 2.63. ПАРАМЕТРЫ LINEARVELOCITY
76
Скорость имеет значение –100 по оси Z. Отрицательность значения зависит от
направления движения. В точке приложения Attachment0 можно увидеть вектор
направления скорости (рис. 2.64). При изменении знака или выбора другой оси
вектор будет меняться.
РИС. 2.64. ВЕКТОР ЛИНЕЙНОЙ СКОРОСТИ
Чтобы тепловоз возвращался в исходную точку по достижении нужных координат,
используются игровые тела с именами A1 и B1 и с отключенными коллайдерами.
Они расположены в противоположных туннелях. Тело A1, как и тело A, находится
по высоте на уровне главных элементов моделей Platf. Это сделано для того, чтобы тепловозы появлялись в этих точках и не проваливались в землю (рис. 2.65).
РИС. 2.65. ТЕЛО А И ТЕЛО A1
77
Для бесконечного движения тепловоза по этому участку дороги создадим скрипт
и добавим его в модель train1. Код схож с предыдущим:
local postA1 = game.Workspace.A1
local postB1 = game.Workspace.B1
local Pivot = postA1:GetPivot()
while wait(0.1) do
local dist = ( (postB1.Position — script.Parent.PrimaryPart.Position).
Magnitude)
if dist < 31 then
wait(1)
script.Parent.PrimaryPart:PivotTo(Pivot* CFrame.Angles(math.rad(10),
0, 0))
end
-- проверка светофора
if game.Workspace.stoplight.Open.Value == true then
script.Parent.Platf.LinearVelocity.VectorVelocity = Vector3.new(0,0,0)
else
script.Parent.Platf.LinearVelocity.VectorVelocity = Vector3.new(0,0,-100)
end
print(dist)
end
Мы выключаем скорость, если загорается зеленый свет светофора. Возможны
смещения модели из-за CanCollide, поэтому туннели и железная дорога имеют
значение CanCollide = false.
Поэкспериментируй с этими функциями.
Теперь затронем симуляцию физических процессов. На самих тепловозах можно
заметить клубы дыма, выходящего из тепловозных труб. Для труб был добавлен
эффект Smoke. Физический эффект всегда добавляется к конкретной детали,
а ее ориентация определяет направление эффекта. На рис. 2.66 представлены
настроенные параметры дыма для тепловоза.
Такой эффект симуляции дыма можно применять ко многим вещам, например
для создания облака. Для этого создадим модель clouds со множеством элементов part и добавим для них эффект Smoke. Сами элементы модели сделаем
прозрачными. На рис. 2.67 показаны параметры Smoke для главной детали
облака cloud.
Эффект плывущих облаков или туман создают атмосферу игре, и в зависимости от жанра можно делать разные туманности. Также с помощью частиц
ParticleEmitter можно воссоздать дождь или снегопад. Для этого достаточно
добавить эффект к детали и настроить текстуру с небольшими настройками
скорости частиц их цвета и т. д. На рис. 2.68 изображено облако с частицами,
имитирующими дождь.
78
РИС. 2.66. НАСТРОЙКА SMOKE ДЛЯ ТЕПЛОВОЗА
РИС. 2.67. ОБЛАКО
79
РИС. 2.68. ДОЖДЕВОЕ ОБЛАКО
Текстуры для частиц можно подгрузить из Toolbox. В примере я использовал
текстуру воды water.
Ты также можешь поэкспериментировать, создать самостоятельно изображение
и опубликовать его. Для этого на странице Create в личном кабинете перейди
в раздел Decal.
РИС. 2.69. ДОБАВЛЕНИЕ СОБСТВЕННОГО ИЗОБРАЖЕНИЯ И ТЕКСТУРЫ
80
Такие облака могут быть статичными или динамичными. Используем для облака скрипт, реализующий его простое движение. Сам скрипт разместим внутри
модели Clouds.
-- Получаем CFrame облака
local Pivot = script.Parent.PrimaryPart.CFrame.Position
print(Pivot)
-- Движение облака по оси Z
while wait(0.1) do
-- движение в положительном направлении
for i = 0, 100 do
script.Parent.PrimaryPart:PivotTo
(CFrame.new(Pivot.X,Pivot.Y,Pivot.Z+i))
wait(0.5)
end
-- движение в отрицательном направлении
for i = 100, 0, -1 do
script.Parent.PrimaryPart:PivotTo
(CFrame.new(Pivot.X,Pivot.Y,Pivot.Z+i))
wait(0.5)
end
end
Еще одно часто используемое физическое явление — это огонь. Как динамическая структура он имеет огромное разнообразие в применении: освещение
(факелы, костры), препятствие (столб пламени, ловушки), сопутствующий
элемент разрушения (пожар, взрыв), формирование эмоционального отклика
(уют и спокойствие, страх, настороженность), элемент временного и пространственного повествования в игре (от первобытного сообщества до эпохи
индустриализации).
Рассмотрим пример костра. Это частый атрибут игр на выживание (Survival),
RPG, экшена (Action). Костры могут быть как инструментом получения пищи, так
и местом отдыха игрового персонажа. Огонь как физический эффект добавляется
непосредственно игровому объекту.
На рис. 2.70 представлена модель костра, созданная из нескольких видимых
цилиндров и невидимого блока, которые содержат эффект Fire. Для каждого
элемента модели эта функция настроена так, чтобы создать целостное пламя.
Огонь характеризуется цветом пламени (начальное и конечное значение), размером пламени (Size), высотой пламени (Heat) и скоростью движения частиц
(TimeScale). Сам огонь не светит (не излучает свет), поэтому для освещения окружения добавим элемент Light: PointLight (точечный источник света с равномерным
освещением), SpotLight (направленный луч света в форме конуса) или SurfaceLight
(свет от одной из граней игрового объекта). В пример добавлен PointLight. На
рис. 2.71 представлены параметры этого источника света. Многие из них есть
и в других источниках света. Параметры PointLight:
Brightnes — степень яркости (интенсивности освещения);
Color — цвет освещения;
Enabled — включение и выключение отображения объекта;
81
Range — расстояние (радиус) освещения;
Shadows — отображение теней от освещаемых предметов.
РИС. 2.70. МОДЕЛЬ КОСТРА И СВОЙСТВО ОСНОВНОГО ПЛАМЕНИ
Параметры Brightnes и Range связаны друг с другом в пропорциональной зависимости.
РИС. 2.71. ПАРАМЕТРЫ POINTLIGHT
82
Итак, ты можешь анимировать почти любые параметры игрового элемента с помощью языка программирования Lua.
ЗАДАНИЯ
Создай лифт с кнопками, имеющими подсказки для его управления.
Создай анимацию включения освещения, когда персонаж встает на пластину
в полу рядом с источником света.
Создай анимацию полета бабочки, танца игрового персонажа.
Создай летающий самолет: взлет, полет в одну сторону, разворот, полет в обратную сторону, посадка.
ИГРОВОЙ ОПЫТ
Никакая игра не обходится без игрового опыта. В игре важно передать игроку
некие навыки или дать ценные для игры предметы. Например, игроку нужно
собрать достаточную сумму игровых денег, чтобы купить меч, способный победить злого волшебника. Или игрок добывает ресурс, а взамен ему открываются
новые возможности по созданию разных предметов и устройств на их основе.
Игра может быть и соревновательного направления, задача которой — хранить
очки, набранные каждым игроком.
Игровой опыт может быть разным. В качестве его хранения выступают глобальные переменные из класса Values, DataStoreService или Open Cloud. Последние два
важны, когда игра хранит ваши достижения и опыт на серверах. Если же игра
подразумевает начисление очков и опыта и их хранение только на время игры,
а после выхода все стирается, то достаточно переменных внутри игры.
ПЕРВЫЙ СПОСОБ НАЧИСЛЕНИЯ ИГРОВОГО ОПЫТА
Рассмотрим простой пример: сбор монеток для покупки меча. Описанные варианты можно найти в файле Leader.rbxl. Для хранения количества монет будем
использовать NumberValue, также можно использовать и IntValue. Подробную
информацию по NumberValue ищи в документации https://create.roblox.com/docs/
reference/engine/classes/NumberValue. Зададим переменной имя Money.
Для примера нужно будет собирать монетки, лежащие на тропинках. Вне тропинок область заполнена космосом (Baseplate с текстурой в виде изображения
космоса). Если мы сойдем с дорожек, то здоровье персонажа будет постепенно
падать, вплоть до полной гибели персонажа — такое условие будет ограничивать игроков в быстром сборе. Для Baseplate добавим скрипт с нанесением урона
игровому персонажу при его касании. Чтобы зафиксировать, что игрок действи-
83
тельно коснулся (вышел в космос), используем логическую переменную BoolValue,
которой зададим имя Space. Переменные Space и Money помещены в Workspace.
Ниже представлен скрипт для Baseplate:
function damage(player)
if player.Parent:FindFirstChild("Humanoid") then
-- задаем переменной значение true
game.Workspace.Space.Value =true
-- наносим урон, если значение true
if game.Workspace.Space.Value == true then
for i = player.Parent.Humanoid.Health, 0, -1 do
player.Parent.Humanoid:TakeDamage(2)
wait(0.2)
end
game.Workspace.Space.Value = false
end
end
end
script.Parent.Touched:Connect(damage)
Если персонаж коснулся области с текстурой космоса, но при этом успел вернуться
на дорожку, то он все равно будет терять здоровье, так как активировалась логика скрипта. На рис. 2.72 представлен пример локации. Желтые элементы — это
монеты. Они представляют собой модели из цилиндра и числа.
РИС. 2.72. ЛОКАЦИЯ ДЛЯ ИГРЫ LEADER
Для полного прохождения нужно собрать сумму в 100 монет, купить меч, найти
ключ от двери, чтобы ее открыть, и победить противника за дверью. Каждая
монета при касании будет передавать переменной Money значение 10 и уничтожаться. Для динамичности добавим в скрипт код для вращения вокруг оси Y.
Чтобы и вращение, и отслеживание касания работали независимо, воспользуемся
RunService. Вот пример такого кода:
84
local Pivot = script.Parent.PrimaryPart:GetPivot()
local RunService = game:GetService("RunService")
--вращение
local angle =0
function rot()
script.Parent.PrimaryPart:PivotTo(Pivot * CFrame.Angles(0,math.rad(angle), 0))
angle +=4
if angle >=360 then
angle =0
end
end
-- касание
function apple_money(player)
if player and player.Parent:FindFirstChild("Humanoid") then
game.Workspace.Money.Value+=10
script.Parent:Destroy()
end
end
-- событие касания
script.Parent.PrimaryPart.Touched:Connect(apple_money)
-- на каждом кадре - прогрузка вращения модели
RunService.Stepped:Connect(rot)
Можно сделать множество таких монет, содержащих скрипт, и проверить, как
они считывают значения. Для этого во время тестирования игры нужно собрать
монеты и проанализировать процесс отображения значений в переменной Money
(рис. 2.73).
РИС. 2.73. СОБИРАЕМ МОНЕТКИ И СМОТРИМ НА ЗНАЧЕНИЕ MONEY
Монеты разбросаны по всей игровой локации, и чтобы добраться до каждой, нужно потратить определенное время. Мы можем дать игроку бонус — быстрый шаг,
если он съест яблоко, лежащее где-то недалеко от места появления (рис. 2.74).
85
РИС. 2.74. БОНУС ДЛЯ БЫСТРОЙ ХОДЬБЫ
В этой модели лежит скрипт, в котором прописана функция передачи значения
на быстрый шаг при касании модели персонажем. За скорость передвижения
персонажей отвечает функция WalkSpeed, принадлежащая классу Humanoid:
function apple_money(player)
if player and player.Parent:FindFirstChild("Humanoid") then
player.Parent.Humanoid.WalkSpeed = 50
script.Parent:Destroy()
end
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
При касании яблока скорость персонажа станет 50 studs в секунду. Если персонаж
попадет в космос и погибнет, то настройки сбросятся. С таким полученным навыком можно быстро перемещаться по локации и собирать монеты.
Модель меча расположим внутри прозрачного бокса в роли указателя. Бокс
будет выполнять роль триггера, поэтому в дальнейшем положим в него скрипт,
отслеживающий касание. При касании будет считываться значение переменной
Money. Если значение будет меньше 100, то прозрачный бокс будет выдавать соответствующий текстовый ответ (рис. 2.75–2.76).
86
РИС. 2.75. ПЕРСОНАЖ НЕ МОЖЕТ КУПИТЬ МЕЧ, ПОКА У НЕГО МАЛО ДЕНЕГ
РИС. 2.76. ПЕРСОНАЖ ПОЛУЧАЕТ МЕЧ, КОГДА ДЕНЕГ ДОСТАТОЧНО
87
На рис. 2.76 видно, что при положительном результате появляется копия меча.
Копия — это инструмент, который можно взять в руки и сохранить в инвентаре.
Подробно о том, как создавать инструменты для инвентаря, читай в книге «Roblox. Играй, программируй и создавай свои миры». Отмечу, что за это отвечает
элемент Tool, а за возможность держать модель в руке — игровой элемент Handle.
Этот инструмент размещается в папке ReplicatedStorage (рис. 2.77).
РИС. 2.77. ИНСТРУМЕНТ — МЕЧ
Чтобы вызвать инструмент в игровой мир, воспользуемся функцией Clone. Чтобы
чат бокса не выдавал огромное количество сообщений в секунду, воспользуемся
переменной n, которая будет считать количество касаний. Если их стало больше
одного, то переменная обнулится при выходе из бокса. Ниже представлен скрипт
для прозрачного бокса.
local chat = game:GetService("Chat")
-- получаем ссылку на инструмент
local sword = game.ReplicatedStorage.Mech
-- счетчик касаний
local n =0
-- функция для проверки монет и выдачи меча
function pod()
n+=1
if game.Workspace.Money.Value >=100 then
game.Workspace.Money.Value -=100
--клонирование инструмента
88
local sword0 = sword:Clone()
sword0.Parent = workspace
-- проверка счетчика
if n==1 then
chat:Chat(script.Parent, "Теперь ты можешь взять меч")
end
else
if n==1 then
chat:Chat(script.Parent, "У тебя недостаточно денег")
end
end
end
-- функция при выходе из триггера бокса
function exit()
n=0
end
-- обработка касаний
script.Parent.Touched:Connect(pod)
script.Parent.TouchEnded:Connect(exit)
Меч нужен, чтобы победить противника. Если персонаж не добудет меч, то при
открытии двери не сможет пройти дальше. Дверь можно открыть, только если
найден ключ в этой локации.
Для ключа я создал глобальную логическую переменную KeyBool. Она примет
значение true, если игровой персонаж возьмет ключ. Для ключа создадим скрипт,
схожий со скриптом для монет, но с той лишь разницей, что он будет передавать
значение на KeyBool вместо Money (рис. 2.78).
РИС. 2.78. ПЕРЕМЕННАЯ KEYBOOL И КЛЮЧ СО СКРИПТОМ
89
Ниже представлен скрипт для ключа. Обрати внимание: ключ — это меш (целостный объект), а не модель, поэтому у него нет PrimaryPart.
local Pivot = script.Parent:GetPivot()
local RunService = game:GetService("RunService")
-- зацикленное движение
local angle =0
function rot()
script.Parent:PivotTo(Pivot * CFrame.Angles(0,math.rad(angle), 0))
angle +=4
if angle >=360 then
angle =0
end
end
function keyGet(player)
if player and player.Parent:FindFirstChild("Humanoid") then
game.Workspace.KeyBool.Value=true
script.Parent:Destroy()
end
end
script.Parent.Touched:Connect(keyGet)
RunService.Stepped:Connect(rot)
Теперь персонаж может брать ключ — он отобразится в значении переменной
KeyBool как true. Получив ключ и меч, можно смело открывать дверь, ведущую
на следующий уровень.
Напишем скрипт для двери. Она может выступать в виде модели: сама дверь
и ручка. При открытии дверь должна поворачиваться вокруг оси Y, проходящей
через точку опоры Pivot. Я уже приводил подобный вариант работы в примере
со шлагбаумом. Здесь в качестве PrimaryPart выбирается сама дверь, а ее точка
опоры сдвигается к краю для корректного вращения (рис. 2.79).
РИС. 2.79. МОДЕЛЬ ДВЕРИ С НАСТРОЙКАМИ ТОЧКИ ОПОРЫ
К ручке двери я добавил всплывающую подсказку, которая дает указание на
удержание кнопки А. Здесь же приложен скрипт для обработки активации на-
90
жатия клавиши. Скрипт лежит внутри модели игрового объекта и использует
общедоступный класс ProximityPromptService. Поэтому если вызывать события
нажатия клавиш без параметров, то они будет активировать и другие игровые
объекты с похожим видом скрипта, использующие этот класс. Чтобы этого не
происходило, можно проверить имя объекта, с которым игрок взаимодействует, а также ID игрока. В скрипте проверяется имя игрового объекта, с которым
взаимодействует игрок, в данном случае с дверной ручкой doorknob. Кроме этого,
проверяется и наличие ключа у игрока. Только при выполнении этих двух условий
дверь откроется. Пример скрипта:
-- обработка событий нажатия клавиши
local Proximity = game:GetService("ProximityPromptService")
-- получаем значения CFrame точки Pivot PrimaryPart
local Pivot = script.Parent.PrimaryPart:GetPivot()
-- Функция открытия двери
function open_door(object)
if ((game.Workspace.KeyBool.Value == true) and (object.Parent.name ==
"doorknob")) then
print("Дверь открывается")
end
for i = 0, 90 do
script.Parent.PrimaryPart:PivotTo(Pivot * CFrame.Angles(0,math.rad(i),0))
wait(0.01)
end
else
print("У вас нет ключа")
end
print(object.Parent)
Proximity.PromptTriggerEnded:Connect(open_door)
В функции есть конечный цикл для плавного открытия двери с помощью класса
CFrame.
За дверью притаился страж — босс игры, с которым придется сразиться. Для
этого персонажу понадобится меч. Но сейчас в руках персонажа меч не динамичный, поэтому нужно добавить скрипт игровому персонажу для запуска
анимации при нажатии левой кнопки мыши. Поэтому нужно использовать
Build RigMesh Rig для создания анимации удара мечом. Из раздела по анимации ты уже знаешь, как это сделать, поэтому нам нужно только записать ее
и опубликовать, а после добавить в функцию Animation, которую разместим
в ReplicatedStorage (рис. 2.80).
Теперь нужно написать LocalScript для запуска анимации по нажатии левой кнопки мыши. Для этого есть как минимум два решения: использовать старый класс
Mouse или новый ContextActionService. Рассмотрим оба варианта, но для начала
создадим локальный скрипт в папке StarterPlayerScripts.
91
РИС. 2.80. СОЗДАНИЕ АНИМАЦИИ ДЛЯ МЕЧА
ПЕРВЫЙ СПОСОБ
-- определяем локального игрока
local player = game:GetService("Players").LocalPlayer
wait(2)
local player0 = player.Character:WaitForChild("Humanoid")
-- находим анимацию
local anim = game.ReplicatedStorage.Sword_play
--подключаем анимацию к игроку
local anim0 = player0:LoadAnimation(anim)
anim0.Looped = false
-- подключаем класс по работе с мышью
local mouse = player:GetMouse()
-- функция включения анимации
function sword()
anim0:Play()
end
-- обработка нажатия левой кнопкой мыши
mouse.Button1Down:Connect(sword)
ВТОРОЙ СПОСОБ
---- определяем локального игрока
local player = game:GetService("Players").LocalPlayer
wait(2)
local player0 = player.Character:WaitForChild("Humanoid")
---- находим анимацию
local anim = game.ReplicatedStorage.Sword_play
----подключаем анимацию к игроку
local anim0 = player0:LoadAnimation(anim)
92
anim0.Looped = false
local ContextActionService = game:GetService("ContextActionService")
-- функция по работе с событиями от пользователя
local function handleAction(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
if actionName == "Sword" then
print("Удар")
anim0:Play()
end
end
end
ContextActionService:BindAction("Sword", handleAction, true,
Enum.UserInputType.MouseButton1)
В зависимости от используемого алгоритма результат будет одинаковым, по
крайней мере на компьютере (рис. 2.81).
РИС. 2.81. УДАР МЕЧОМ ПРИ НАЖАТИИ ЛЕВОЙ КНОПКИ МЫШИ
При использовании такого алгоритма могут возникнуть ошибки при долгой загрузке игрока на сервер игры. Решить это можно с помощью задержки по времени
либо проверять добавление игрока в самой функции.
Пример:
local ContextActionService = game:GetService("ContextActionService")
local function handleAction(actionName, inputState, inputObject)
local player = game:GetService("Players").LocalPlayer
local player0 = player.Character:WaitForChild("Humanoid")
-- находим анимацию
93
local anim = game.ReplicatedStorage.Sword_play
--подключаем анимацию к игроку
local anim0 = player0:LoadAnimation(anim)
anim0.Looped = false
if inputState == Enum.UserInputState.Begin then
if actionName == "Sword" then
print("Удар")
anim0:Play()
end
end
end
ContextActionService:BindAction("Sword", handleAction, true,
Enum.UserInputType.MouseButton1)
Изменим название лезвия меча на Blade, чтобы по имени определить, какой
именно элемент бьет стража (рис. 2.82).
РИС. 2.82. ПЕРЕИМЕНОВАНИЕ ЛЕЗВИЯ МЕЧА
В проекте Leader.rbxl представлен страж, который вращается благодаря элементу
Torque. У стража есть лучи, которыми он и наносит урон игроку. Игровой персонаж может наносить урон стражу с помощью меча, ударяя того по лучам. После
десяти точных ударов страж исчезнет. Для хранения в количественной форме
жизненной энергии стража используем переменную типа IntValue с именем Life.
Укажем значение для переменной равным десяти. Для каждого луча добавим
скрипт по нанесению урона игровому персонажу и получения урона от лезвия:
94
function damage(object)
if script.Parent.Parent.Life.Value <=0 then
script.Parent.Parent:Destroy()
end
if object.Parent:FindFirstChild("Humanoid") then
-- наносим урон
object.Parent.Humanoid:TakeDamage(2)
wait(0.2)
end
-- получает урон
if object.name =="Blade" then
script.Parent.Parent.Life.Value -=1
end
end
script.Parent.Touched:Connect(damage)
Возьмем меч с ключом и протестируем бой с противником (рис. 2.83). Во время
тестирования можно открыть переменную Life, чтобы увидеть, как изменяются
значения.
РИС. 2.83. СТРАЖ И ЕГО ЖИЗНЕННАЯ ЭНЕРГИЯ
Весь опыт и достижения игрока хранятся в самой игре. Для наглядности опыт
игрока отображается в графическом интерфейсе — подробнее о нем расскажу
в соответствующем разделе. Представленный вариант игровых элементов и механика игры — это только пример. Тебе решать, что ты будешь использовать
и как реализовывать.
ВТОРОЙ СПОСОБ НАЧИСЛЕНИЯ ИГРОВОГО ОПЫТА
Кроме хранения достижений в самой игре опыт игрока можно хранить и на
сервере. В этом поможет технология DataStoreService, а посмотреть документацию по ней можно здесь: https://create.roblox.com/docs/reference/engine/classes/
DataStoreService. Об этой технологии хранения данных я подробно рассказывал
в книге «Roblox. Играй, программируй и создавай свои миры».
95
Для облачного хранения создадим папку для конкретного игрока, где и будем
хранить переменные, значения которых могут быть записаны в виде таблицы.
Есть общедоступная папка leaderstats, которая будет видна всем игрокам. Так они
смогут сравнить свои результаты с другими.
Опубликуем нашу игру на сервере и откроем доступ к передаче данных (рис. 2.84–
2.85).
РИС. 2.84. ПУБЛИКАЦИЯ ПРОЕКТА В ОБЛАКО
РИС. 2.85. ВКЛЮЧЕНИЕ ПЕРЕДАЧИ ДАННЫХ МЕЖДУ СЕРВЕРОМ И ПК
96
Как видно из рис. 2.86, настройки можно найти в Game Setting в меню Home. Обрати внимание, что значок игрового проекта теперь в виде облака — это говорит
о том, что игра опубликована.
Теперь на примере хранения монет напишем код для облачного хранения данных.
Скрипт разместим в папке ServerScriptService и назовем DataScript.
-- подключение к папке, где хранятся подключенные игроки
local player = game:GetService("Players")
-- подключение к сервесу хранения данных и создание таблицы Progress
local progressStore = game:GetService("DataStoreService"):GetDataStore("Progress")
-- создаем папку для хранения данных каждому подключенному игроку
function DataPlayer(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local money = Instance.new("NumberValue", leaderstats)
money.Name = "Money"
money.Parent = leaderstats
end
-- как только игрок подключился, запускаем функцию создания
player.PlayerAdded:Connect(DataPlayer)
Это начало скрипта.
Проверим его работу — сохраним проект и еще раз опубликуем, затем запустим
тестовый режим (рис. 2.86).
РИС. 2.86. ОТОБРАЖЕНИЕ ДАННЫХ В ПАПКЕ LEADERSTATS
97
Если у тебя отображается все так же, как и на рис. 2.86, значит, все сделано
правильно.
Теперь в скрипте создадим переменные, которые будут хранить значение и ключ.
В нашем случае ключ — это ID игрока, подключенного к игре, а значение — количество монет. Причем при подключении игрока будут считываться значения по
ключу, которые передадутся на переменную money. Этот алгоритм дописывается
в ту же функцию, что была создана ранее.
Тусклым шрифтом я выделил места в коде, между которыми нужно добавить
новый фрагмент:
money.Parent = leaderstats
-- создание ключа
local keyMoney = "Money"..player.UserId
print (keyMoney)
-- создание значения
local dataMoney
-- подключаемся для чтения данных значения.
local success, err = pcall(function()
dataMoney = progressStore:GetAsync(keyMoney)
end)
if success then
money.Value = dataMoney
else
print("Error connect ...")
end
end
-- как только игрок подключился, запускаем функцию создания
player.PlayerAdded:Connect(DataPlayer)
Теперь перейдем к скриптам монет, разбросанных по игровой локации. Допишем
код для возможности добавлять значение переменной Money в папке лидеров.
Для этого добавим строку для подключения к папке Players.
local Players = game:GetService("Players")
Затем в функцию apple_money, где происходит начисление монет, добавим строки
для поиска папки leaderstats у нашего игрока и для нахождения переменной Money.
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
player0:WaitForChild("leaderstats").Money.Value +=10
-- удаляем монету
Добавив эти строки каждому скрипту, прикрепленному к монете, перейдем к их
тестированию. При касании монета должна исчезать, а в переменную Money
таблицы лидеров будет начисляться +10 (рис. 2.87).
98
РИС. 2.87. НАЧИСЛЕНИЕ МОНЕТ В ТАБЛИЦУ ЛИДЕРОВ
Теперь нужно сохранить достижения на сервер, чтобы при выходе и повторном
входе в игру значения не терялись. Вернемся в скрипт DataScript и создадим обработчик выхода игрока.
function SavePlayer(player)
-- создание ключа
local keyMoney = "Money"..player.UserId
print (keyMoney)
-- сохранение значения
local dataMoney = player.leaderstats.Money.Value
-- подключаемся для записи данных значения
local success, err = pcall(function()
progressStore:SetAsync(keyMoney, dataMoney)
end)
if success then
print("Data Store")
else
print("Error connect ...")
warn(err)
end
end
-- при выходе игрока сохраняются данные
player.PlayerRemoving:Connect(SavePlayer)
Перезапись происходит каждые 6 секунд, поэтому рекомендуется подождать это
время, если у вас нет кнопок по сохранению данных. Если при выходе в окне
Output появилась надпись Data Store, то, значит, сохранение прошло успешно.
Осталось списать монеты при покупке меча. Для этого достаточно исправить
вариант кода для прозрачного триггера с мечом из прошлого проекта.
99
local Players = game:GetService("Players")
local chat = game:GetService("Chat")
-- получаем ссылку на инструмент
local sword = game.ReplicatedStorage.Mech
-- счетчик касаний
local n =0
-- функция для проверки монет и выдачу меча
function pod(player)
n+=1
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения монет из сервера в глобальную переменную
game.Workspace.Money.Value = player0:WaitForChild("leaderstats").Money.Value
-- проверка условия на покупку меча
if player0:WaitForChild("leaderstats").Money.Value>=100 then
player0:WaitForChild("leaderstats").Money.Value -=100
game.Workspace.Money.Value -=100
--клонирование инструмента
local sword0 = sword:Clone()
sword0.Parent = workspace
-- проверка счетчика
if n==1 then
chat:Chat(script.Parent, "Теперь ты можешь взять меч")
end
else
if n==1 then
chat:Chat(script.Parent, "У тебя недостаточно денег")
end
end
end
-- функция при выходе из триггера бокса
function exit()
n=0
end
-- обработка касаний
script.Parent.Touched:Connect(pod)
script.Parent.TouchEnded:Connect(exit)
Согласно скрипту, меч появится в той точке workspace, где был создан до его
переброски в ReplicatedStorage. Также можно отправлять меч непосредственно
в рюкзак игрового персонажа при совершении покупки. Есть как минимум два
варианта размещения предмета в инвентарь.
Первый:
cloneCube.Parent = player:FindFirstChildOfClass("Backpack")
Второй:
cloneCube.Parent = player.Backpack
Важно, чтобы предмет был оформлен как Tool.
На этом я заканчиваю введение в особенности добавления опыта и достижений
игрока в Roblox.
100
ЗАДАНИЯ
Создай модель пружины. Если игрок коснется ее, она исчезнет и передаст
свойство на высокие прыжки в 30 или 50 studs.
Создай двигающиеся объекты (мячики, звездочки, бананы), которые падают
или движутся по горизонтали в заданной области. Задача игрока — собрать
их и получить бонусы в виде очков (баллов).
Добавь в таблицу лидеров дополнительные переменные, отвечающие за подсчет очков из предыдущего задания и за наличие ключа в инвентаре.
ЗВУКИ, ЗВУКОВЫЕ ЭФФЕКТЫ В ИГРАХ
Звуки играют важную роль в играх. Они позволяют игроку погрузиться в атмо
сферу созданного мира. Ты можешь как подгружать свои аудиофайлы, так и импортировать их из Toolbox. Документацию по работе с аудиофайлами читай по
ссылке https://create.roblox.com/docs/building-and-visuals/audio/audio-assets#importingcustom-audio.
Я продемонстрирую работу со звуком на примере опубликованной игровой локации Game Arcade из предыдущего раздела. Звуки могут быть как в виде фоновых
мелодий для игровых зон, так и короткими элементами, описывающими событие.
Звуки можно подгрузить из Toolbox (рис. 2.88).
РИС. 2.88. ЗВУКИ В TOOLBOX
101
В категории Audio ты можешь выбрать и звук эффекта, и фоновую музыку. Звуки
распределены по категориям. Каждый звуковой файл можно предварительно
прослушать, а потом импортировать через Insert или скопировать ID звука для
анимации.
Если ты хочешь загрузить свой аудиофайл, перейди на страницу Roblox в личный
кабинет, открой вкладку Create и выбери категорию Audio. Обрати внимание,
что неподтвержденным пользователям разрешается загружать не более 10 файлов в месяц (рис. 2.89). Подтвержденным — до 100 файлов в месяц. Аудиофайл
должен быть в формате .mp3 или .ogg, не должен превышать 19,8 Мбайт, а его
длительность должна быть не более 7 минут.
РИС. 2.89. ЗАГРУЗКА ЗВУКОВОГО ФАЙЛА
Как и другие проекты, загруженные аудиофайлы будут доступны через Roblox
Studio (рис. 2.90). Если ты добавляешь этот файл в свой проект первый раз, то
программа попросит разрешения на добавление (красная надпись в окне Output).
Чтобы разрешить встроить звуковой файл в игру, кликни по этой красной
надписи — появится окно (рис. 2.91).
Так происходит только для неподтвержденных аккаунтов. Подтвердить свой
аккаунт, то есть возраст, можно только после достижения 14 лет при наличии
удостоверения личности. Либо сами модераторы Roblox определят тебя как подтвержденного разработчика https://en.help.roblox.com/hc/en-us/articles/7997207259156Verified-Badge-FAQ#criteria_table.
102
РИС. 2.90. ЗАГРУЖЕННЫЙ АУДИОФАЙЛ В ROBLOX STUDIO
РИС. 2.91. РАЗРЕШЕНИЕ НА ДОБАВЛЕНИЕ ЗВУКА В ИГРУ
Для примера я возьму два аудиофайла из Toolbox (Mondial Space 2 и Sword Swing
Metal Heavy) и один загруженный collection, имитирующий звук сбора предметов.
Их ID: 9048678603, 6241709963, 11342553209. Первый — фоновая музыка, второй — звук удара меча, третий — сбор предметов. Первый и последний файлы
мы разместим прямо на сцене Workspace и добавим скрипт Melody, который будет
включать мелодию.
-- определяем звуковой файл в игре и включаем его
local sound = game.Workspace["Mondial Space 2"]
-- зацикливаем воспроизведение
sound.Looped = true
-- устанавливаем скорость воспроизведения
sound.PlaybackSpeed = 0.5
-- включаем проигрывание мелодии
sound.Playing = true
При входе в игру мы услышим фоновую музыку. У звука много параметров, которые можно настроить, полную документацию по звуку смотри здесь: https://
create.roblox.com/docs/reference/engine/classes/Sound.
103
Разместим последний звуковой файл в папке ReplicatedStorage (рис. 2.92).
РИС. 2.92. ЗВУКИ В REPLICATEDSTORAGE
Свяжем звук меча и нажатие кнопки мыши. Для этого перейдем в наш локальный скрипт для игрока, где было прописано событие нажатия мыши, и добавим
запуск звука при нажатии кнопки.
Ниже представлен фрагмент кода, куда нужно добавить алгоритм запуска звука.
Тусклым шрифтом отмечен уже написанный в предыдущем разделе код.
local sound_sword = game.ReplicatedStorage["Sword Swing Metal Heavy"]
...
if inputState == Enum.UserInputState.Begin then
if actionName == "Sword" then
print("Удар")
anim0:Play()
-- звук удара мечом
wait(0.5)
sound_sword.Playing = true
end
end
Теперь при нажатии левой кнопки мыши мы можем услышать звуки удара мечом.
Осталось добавить звуки сбора монет. Так как монеты исчезают при касании, то
размещать в них код по выводу звука будет неправильно. В скриптах для монет
мы пропишем отправку значения на глобальную логическую переменную. Если
персонаж коснулся монеты, то переменная принимает значение true. В свою
очередь в скрипте Melody мы можем создать цикл, который проверяет значение
логической переменной и включает звук, когда она true.
Для изменения значения логической переменной мы можем создать ModuleScript
в папке ServerScriptService (рис. 2.93). Этот вид скрипта используется для удобства
многократного использования алгоритма, который вызывают другим скриптом.
Наш модульный скрипт, или просто модуль, будет содержать следующие строки:
104
-- конструкция модуля
local module = {}
--функция модуля
function module.playMoney()
game.Workspace.SoundBool.Value
print("Запуск звука")
end
= true
return module
РИС. 2.93. МОДУЛЬ СКРИПТА
Теперь добавим логическую переменную в Workspace под именем SoundBool
(рис. 2.94).
РИС. 2.94. ЛОГИЧЕСКАЯ ПЕРЕМЕННАЯ ДЛЯ ВКЛЮЧЕНИЯ ЗВУКА
105
Проверим значение переменной в скрипте Melody и допишем код:
while wait(0.1) do
if game.Workspace.SoundBool.Value == true then
print("Поехали")
game.Workspace["collection"].Playing = true
wait(0.5)
game.Workspace.SoundBool.Value = false
end
end
Осталось добавить код по вызову модуля при касании с монетой. В каждый
скрипт, привязанный к монете, добавим строку внутри функции apple_money
скрипта:
-- запуск мелодии
local col = require(game.ServerScriptService.ModuleScript)
col.playMoney()
Теперь сохраним проект, еще раз опубликуем его в Roblox и протестируем.
ЗАДАНИЕ
Добавь и запрограммируй вызов звука при открытии двери, взятии ключа
и яблока.
Добавь и запрограммируй вызов звука при нанесении урона от стражника.
ГРАФИЧЕСКИЕ ИНТЕРФЕЙСЫ
ПОЛЬЗОВАТЕЛЯ И ДИАЛОГИ
Тестовый вариант нашей первой игры почти готов — осталось добавить графический интерфейс для пользователя, чтобы игрок получал исчерпывающую
информацию по игре.
В книге «Roblox. Играй, программируй и создавай свои миры» я уже делал большой обзор по инструментарию GUI (графический пользовательский интерфейс).
Здесь же быстро пройдемся по процессу создания примера GUI для нашей тестовой игры.
Для начала создадим три иконки: монету, ключ и меч. Они будут заменять словесное описание и олицетворять игровые элементы, которые нужно искать по
заданию. Эти иконки я сделал с помощью инструмента Screenshot (снимок экрана), где отображал модели меча, ключа и монеты.
Снимок экрана сохраняет все элементы на экране, а фон у моделей нужно убрать
для красоты. Ты можешь воспользоваться любыми графическими редакторами,
106
умеющими вырезать фон, либо онлайн-инструментами, автоматически удаляющими фон. Примеры иконок представлены на рис. 2.95.
РИС. 2.95. ИКОНКИ ДЛЯ ИГРЫ
Теперь загрузим иконки через личный кабинет во вкладке Create (рис. 2.96).
Перейдем в проект: в примере указан проект Game Arcade. В окне Toolbox во
вкладке своих инструментов ты найдешь добавленные иконки (рис. 2.97).
РИС. 2.96. ДОБАВЛЕНИЕ ИКОНОК
107
РИС. 2.97. ИКОНКИ В ОКНЕ TOOLBOX
Если щелкнуть по любой картинке правой кнопкой мыши, скопируется ID. Теперь
начнем настраивать интерфейс с помощью ID и разместим все в папке StarterGui.
Определим границы расположения иконок, для чего воспользуемся элементом
Frame. Сделаем ее прозрачной, поставим параметру BackgroundTransparency значение 1. В этой рамке разместим три ImageLabel и три TextLabel. Переименуем
добвленные ImageLabel и TextLabel в ImageMoney, ImageKey, ImageSword, TextMoney,
TextSword, TextKey.
Привяжем наши иконки с помощью ID, который добавим параметру Image элемента ImageLabel. На рис. 2.98 представлен пример размещения этого интерфейса.
РИС. 2.98. НАСТРОЙКА ГРАФИЧЕСКОГО ИНТЕРФЕЙСА
108
Настройка прозрачности TextLabel происходит через тот же параметр, а настройка
цвета и размера шрифта — в разделе Text. Теперь протестируем процесс отображения в игре. Если интерфейс находится не на желаемой высоте или не так
смещен по горизонтали, исправим это в свойстве Frame. Перемещая рамку, мы
перемещаем и все то, что принадлежит ей.
Этот интерфейс должен работать на пользователя и, следовательно, должен
отображать текущее значение в игре. Добавим элемент Script текстовым полям
TextMoney, TextSword, TextKey. В них будем считывать данные с глобальных переменных, хранящих значение по монетам, ключу и мечу. В проекте уже есть переменные Money, KeyBool, теперь добавим и переменную SwordBool.
В скрипт для магазина, где персонаж покупает меч, добавим строку, указывающую значение true переменной SwordBool, если меч куплен. Эту строку мы добавим в условие, где списываются 100 монет.
game.Workspace.SwordBool.Value = true
В локальном скрипте игрока в папке StarterPlayerScripts можно дополнительно
отправлять данные по монетам и наличию меча (если такая переменная есть)
из папки leaderstats в глобальные переменные Money и SwordBool.
Пример:
local Players = game:GetService("Players")
function datalocal(player)
--print(Players.LocalPlayer)
game.Workspace.Money.Value = Players.LocalPlayer:WaitForChild("leaderstats").
Money.Value
game.Workspace.SwordBool.Value = Players.LocalPlayer:WaitForChild
("leaderstats").Sword.Value
end
runserv.Stepped:Connect(datalocal)
Теперь приступим к написанию кода для наших текстовых полей UI. Вот простой,
но не слишком продуктивный вариант.
Для TextKey:
while wait(0.1) do
script.Parent.Text =tostring(game.Workspace.KeyBool.Value)
end
Для TextMoney:
while wait(0.1) do
script.Parent.Text = tostring(game.Workspace.Money.Value)
end
Для TextSword:
while wait(0.1) do
script.Parent.Text =tostring(game.Workspace.SwordBool.Value)
end
109
В разделе «Экономика в играх» мы рассмотрим, как использовать в подобных
случаях Remote Events. Но для текущих целей код, представленный выше, нам
пока подойдет.
Ниже даны примеры отображения в пользовательском интерфейсе (рис. 2.99
и 2.100).
РИС. 2.99. СОБРАНЫ 8 МОНЕТ И КЛЮЧ
РИС. 2.100. КУПЛЕН МЕЧ И ПОДОБРАН КЛЮЧ. ОТОБРАЖАЕТСЯ ОСТАТОК ДЕНЕГ
110
Теперь добавим поясняющий для игрока текстовый Label. Он должен отобразиться
в начале игры и исчезнуть по истечении 15 секунд. На рис. 2.101 показан пример
такого пояснения.
РИС. 2.101. ПОДСКАЗКА ДЛЯ ИГРОКА В LABEL С ИМЕНЕМ TASK
Для этого Label добавим скрипт, который уберет текст (закроет Label) через
15 секунд.
wait(15)
script.Parent.Visible = false
На этом знакомство с графическим интерфейсом завершено.
ЗАДАНИЕ
Создай свои графические значки для интерфейса и размести их.
Создай анимированный графический интерфейс с помощью скрипта.
Открой свою любимую игру и попробуй скопировать ее интерфейс в свой
проект.
111
ГЕНЕРАЦИЯ ИГРОВЫХ ОБЪЕКТОВ
И ЦЕЛЫХ ИГРОВЫХ МИРОВ
В некоторых играх бывает механика генерации (клонирования) объектов. Например, появление ресурсов через определенное время, возрождение противников,
генерация расходуемого материала, зданий и сооружений. Возможны генерации
целых игровых локаций по определенным алгоритмам вместо ручного создания
(как в игре Minecraft).
ГЕНЕРАЦИЯ ИГРОВЫХ ОБЪЕКТОВ
В нашем проекте Game Arcade нет генерации игровых объектов — и это критично, если в игру зайдет не один игрок, а два или более. Кому-то может не хватить
монет, ключа и, следовательно, меча.
Как это исправить? При удалении того или иного предмета можно отправлять
запрос на его генерацию в случайной точке определенной области. Для этого
монету и ключ можно продублировать в папке ReplicatedStorage. Как только мы
заберем монетку, то в глобальную переменную логического типа SpawnMoney
добавим значение true, которое будем считывать с помощью скрипта SpawnScript
из папки ServerScriptService.
Добавим во все скрипты монет на сцене код перед удалением их при касании.
--активируем логическую переменную
game.Workspace.SpawnMoney.Value = true
-- удаляем монету
script.Parent:Destroy()
Также добавим глобальную логическую переменную SpawnMoney в Workspace.
Такую же операцию проведем для игрового объекта key и добавим код в его
скрипт в функцию keyGet:
--сигнал на создание клона
game.Workspace.SpawnKey.Value = true
--удаление ключа
script.Parent:Destroy()
Нужно создать логическую переменную SpawnKey, поэтому выберем области для
произвольного появления монеты и ключа. Так как бˆольшая часть территории
в нашем проекте непригодна для ходьбы, то игровые объекты остается размес
тить на дорожках. Выберу три: две параллельные и одну перпендикулярную им.
Определим координаты их границ и запишем в скрипт под именем SpawnScript
в папке ServerScriptService.
112
-- координаты первой области
local x00 = 816
local x01 = 848
local z00 = -948
local z01 = 700
--координаты второй области
local x10 = -1016
local x11 = 990
local z10 = -33
local z11 = 0
--координаты третьей области
local z20 = -1020
local z21 = 990
local x20 = -148
local x21 = -112
Чтобы клонировать (генерировать) монеты и ключи, разместим по одной их
модели в папке ReplicatedStorage (рис. 2.102).
РИС. 2.102. ЭЛЕМЕНТЫ ДЛЯ КЛОНИРОВАНИЯ
Осталось дописать в скрипт SpawnScript логику клонирования игровых объектов
из папки ReplicatedStorage в точку с координатами одной из трех областей. Ниже
представлен пример кода этого алгоритма.
local run = game:GetService("RunService")
local money = game.ReplicatedStorage.money
local key = game.ReplicatedStorage.key
-- координаты клонированных монет
local xm
113
local zm
local cloneMoney
-- координаты клонирования ключа
local cloneKey
local xk
local zk
-- клонирование монет и ключа
function spawnMoneyKey()
-- проверка на клонирование монеты
if game.Workspace.SpawnMoney.Value == true then
local n = math.random(1,3)
-- кординаты для монеты
if n == 1 then
xm = math.random(x00,x01)
zm = math.random(z00,z01)
elseif n == 2 then
xm = math.random(x10,x11)
zm = math.random(z10,z11)
elseif n == 3 then
xm = math.random(x20,x21)
zm = math.random(z20,z21)
end
-- клон монеты
cloneMoney = money:Clone()
cloneMoney.PrimaryPart:PivotTo(CFrame.new(xm, 2, zm))
cloneMoney.Parent = workspace
game.Workspace.SpawnMoney.Value =false
end
-- проверка на клонирование ключа
if game.Workspace.SpawnKey.Value == true then
local n = math.random(1,3)
-- координаты для ключа
if n == 1 then
xk = math.random(x00,x01)
zk = math.random(z00,z01)
elseif n == 2 then
xk = math.random(x10,x11)
zk = math.random(z10,z11)
elseif n == 3 then
xk = math.random(x20,x21)
zk = math.random(z20,z21)
end
-- клон ключа
cloneKey = key:Clone()
cloneKey:PivotTo(CFrame.new(xk, 2, zk))
cloneKey.Parent = workspace
game.Workspace.SpawnKey.Value = false
end
end
run.Stepped:Connect(spawnMoneyKey)
Теперь любой игрок может быть уверен, что наберет достаточную для покупки
меча сумму и найдет ключ, чтобы открыть дверь.
114
Генерировать объекты можно с использованием разных событий, например по
нажатии клавиш или достижении определенного значения, а также входа (касания) области. Подобных событий можно придумать множество.
ГЕНЕРАЦИЯ ЛОКАЦИЙ
Мы рассмотрели генерацию игровых объектов, но в Roblox можно генерировать
и целые локации (карты). Самый простой способ — использовать инструмент
Terrain Editor , который позволяет генерировать ландшафт как вручную, так
и автоматически по указанным свойствам. Эти два простых способа подробно
рассмотрены в книге «Roblox. Играй, программируй и создавай свои миры».
Здесь же мы рассмотрим более продвинутый вариант генерации миров с помощью импортирования изображения как непосредственно через Terrarian Editor,
так и с использованием своего алгоритма генерации локации по изображению.
Рассмотрим первый вариант: импортируем изображение ландшафта местности
в Terrarian Editor. Для этого понадобится изображение не более 4096 × 4096 пикселей. В качестве примера я взял изображение местности Гималаев на Google
Maps (рис. 2.103).
РИС. 2.103. СКРИНШОТ МЕСТНОСТИ ГИМАЛАЕВ НА GOOGLE MAPS
115
Перейдем в окно Terrarian Editor и выберем инструмент Import: здесь мы и загрузим
наше изображение. Я использую скриншот 800 × 600 пикселей. Размер изображения будет влиять в последующем на отображение местности. При загрузке
программа выставит разрешение местности и создаст карту высот.
Карта высот — это черно-белая версия исходного изображения, предназначенная
для автоматического присвоения высоты по значению пикселя.
В черно-белом изображении каждый пиксель имеет одно значение, а не три, как
в цветном. Это значение варьируется от 0 до 255. Черный цвет — это 0, а чистый
белый — 255. Между чистым черным и чистым белым есть промежуточные цвета — градации серого. Чем ярче цвет, тем больше значение и тем выше будет
эта область. Если генерировать карты по размеру изображения, то с большой
вероятностью местность будет выглядеть как непричесанный еж: будет очень
много острых конструкций, что не всегда хорошо.
Один из способов сглаживания — увеличение размеров генерируемой области
по отношению к изображению. Обычно для таких задач я выбираю размер местности в 2–5 раз больше исходного размера загруженной карты.
Мы разобрались с картой и выбором размеров, теперь перейдем к настройке
цвета для карты. Есть два варианта: выбрать конкретный материал или использовать цветную карту, чтобы генератор автоматически подобрал по ней цвет
из тех материалов, которыми располагает. Выбрав нужную опцию, щелкнем по
кнопке Generate. Пример такой реализации показан на рис. 2.104 и 2.105.
РИС. 2.104. ГЕНЕРАЦИЯ ИГРОВОЙ ЛОКАЦИИ ПО КАРТЕ С ПОМОЩЬЮ IMPORT
116
РИС. 2.105. СРАВНЕНИЕ РЕЗУЛЬТАТА И ИСХОДНОГО ИЗОБРАЖЕНИЯ
После генерации выйдем из режима, закрыв окно Terrain Editor.
По этой карте можно перемещаться и исследовать ее области. Но такая генерация
не всегда точно может передать цвет ландшафта. Полную картину огромного
ландшафта можно увидеть только с высоты птичьего полета. Игрок не будет
видеть картину целиком.
Кроме встроенного генератора локации по изображению можно создать свой.
Для этого нужно получить значение каждого пикселя исходного изображения
и его черно-белого варианта. Так как разработчики Roblox Studio ограничили возможность работы с файлами напрямую и урезали функциональность
встроенной библиотеки os, то для решения задачи понадобятся другие языки. Например, Python. По этому языку есть много информации, и его легко
установить.
Мы установим три внешние библиотеки — OpenCV, numpy и pillow. Первая и третья работают с изображениями и компьютерным зрением, а вторая — с матрицами. Библиотека numpy преобразует исходное изображение в виде матрицы
из значений RGB каждого пикселя, а библиотека OpenCV преобразует цветное
изображение в черно-белое для создания карты высот.
Простой редактор Python можно установить через официальный сайт www.python.org.
После установки перейди в командную строку: ПускСлужебныеКомандная
строка.
Пропишем загрузку библиотек с помощью установщика pip:
pip install pillow
pip install numpy
pip install opencv-python
117
Пропиши первую библиотеку, нажми Enter и дождись загрузки. Затем проделай то же самое со второй и третьей библиотеками. Затем закрой командную
строку.
Теперь можно открыть файл Python: ПускPython <версия>IDLE (Python <версия>).
В открывшемся окне создадим программу по работе с изображением. Рекомендую
сохранить файл программы в том же месте, где расположено исходное изображение: нажми FileSave, выбери место сохранения и назови файл.
В этом примере я экспериментирую с изображением World0.png. Для быстроты
работы программы я уменьшил его до 250 × 180 пикселей.
В названии указано расширение файла: поскольку у изображений могут быть
разные расширения, в программе нужно его указывать. Ниже представлен пример такой программы.
Import numpy as np
from PIL import Image
import cv2
# открываем изображение и преобразуем его в матрицу
window = np.array(Image.open('world0.png'))
# создаем текстовый файл для хранения
# цвета каждого пикселя в RGB-формате
color = open('color.txt', 'w')
# записываем цвет пикселя в файл
for I in range(len(window)):
color.write('{')
for j in range(len(window[i])):
color.write('{')
for k in range(len(window[i][j])):
if k!=len(window[i][j])-1:
color.write(str(window[i][j][k])+',')
else:
color.write(str(window[i][j][k]))
if j!=len(window[i])-1:
color.write('},')
else:
color.write('}')
color.write('},')
color.write('\n')
# закрываем файл по окончании
color.close()
# преобразуем изображение в черно-белое
im =cv2.cvtColor(window, cv2.COLOR_BGR2GRAY)
# создаем текстовый файл для хранения
# одно значение каждого пикселя
f = open('text.txt', 'w')
# записываем значение пикселя в файл
118
for i in range(len(im)):
f.write('{')
for j in range(len(im[i])):
if j!=len(im[i])-1:
f.write(str(im[i][j])+',')
else:
f.write(str(im[i][j]))
f.write('},')
f.write('\n')
# закрываем файл
f.close()
Кроме значений пикселей добавляем фигурные скобки. В Lua списки, кортежи,
массивы и матрицы объединены в тип данных «таблицы», о которых я рассказывал в первой книге. Таблицы определяются фигурными скобками.
Чтобы запустить программу, достаточно нажать клавишу F5 или выбрать команду Run в окне редактора. Если в программе нет ошибок, то в том месте на диске,
где мы ее сохранили, появятся два текстовых файла, color.txt и text.txt. В первом
файле будет храниться цвет каждого пикселя, а во втором — значения пикселей
черно-белого изображения (карта высот).
Теперь переместим эти значения в скрипт в проекте Roblox Studio. Проект назовем Generate_Map_World.rbxl. Создадим простой объект Part с размером (1,1,1), разместим его в точке (–1000, 0.5, –250) и переместим его в папку ReplicatedStorage.
Затем создадим Script в папке Workspace и назовем его Script_world.
С помощью этого скрипта мы создадим для каждого пикселя изображения клон
элемента Part и зададим ему размер (5, Height, 5), где Height — это значение пикселя с карты высот. Чтобы высоты не колебались от 0 до 255 и не давали слишком
больших перепадов, каждое значение можно делить на 2. Созданные клоны будем
группировать в Model.
Напишем суть алгоритма и сделаем отступ в 2–3 строки, чтобы выше добавить
две таблицы. Алгоритм будет проходить двумерную таблицу, в которой хранятся
значения карты высот, и строить на каждом шаге со смещением в 5 studs клон
Part. Размер клона будем брать из этой таблицы. Таблица будет называться map.
Как только мы создадим клон и укажем его размер, его нужно будет закрепить
с помощью Anchored и установить цвет, который мы определим из второй таблицы color.
Вот как выглядит алгоритм:
local model = Instance.new("Model",workspace)
local cube1 = game.ReplicatedStorage.Part
for j = 1, 180 do
for I = 1, 250 do
119
end
end
local cube = cube1:Clone()
--указываем размер из таблицы map
cube.Size = Vector3.new(5,map[j][i]/2,5)
cube.Position += Vector3.new(5*I,0,5*j)
cube.Parent = model
cube.Anchored = true
--указываем цвет из таблицы color
cube.Color = Color3.new(color[j][i][1]/255,color[j][i][2]/255,
color[j][i][3]/255)
Два цикла имеют те же значения, что и размер изображения: 250 × 180. Это
сделано не случайно: нужно проходить столько шагов, сколько пикселей в изображении. Если шагов будет больше, а пикселей меньше, то может возникнуть
ошибка. Осталось добавить в самом начале скрипта две пустые таблицы, map
и color.
Для таблицы map нужно скопировать все значения из файла text.txt. Есть быстрый
способ выполнить эти действия с помощью комбинации клавиш:
выделить все: Ctrl + A;
скопировать: Ctrl + C.
Затем в пустые {} таблицы вставляем скопированное: Ctrl + V.
Пример результата на рис. 2.106.
РИС. 2.106. ФРАГМЕНТ ТАБЛИЦЫ MAP
Аналогичную процедуру проводим для таблицы color: копируем значения из
файла color.txt (рис. 2.107).
120
РИС. 2.107. ФРАГМЕНТ ТАБЛИЦЫ COLOR
Теперь приступим к тестированию. Если все сделано правильно, то загрузится
карта на основе нашего изображения (рис. 2.108).
РИС. 2.108. КАРТА ЛОКАЦИИ: РЕЗУЛЬТАТ И ОРИГИНАЛ
Мы видим, что цветопередача полностью сохранилась. Значение блоков по x и z
взяты как 5 × 5 не просто так. Изображение со спутника охватывает огромную
территорию. Поэтому, когда мы создаем карту по такому изображению в Roblox,
то территория должна быть сопоставимо бˆольших размеров по отношению
к игровому персонажу.
Для тестирования ты всегда сможешь скачать этот проект с моего репозитория
на GitHub. Здесь будет еще один пример: карта части города в виде скрипта
121
ScriptCiTy. Этот скрипт по умолчанию лежит в папке ReplicatedStorage, и чтобы его
включить, нужно либо поменять местами скрипты в Workspace и ReplicatedStorage,
либо написать скрипт для вызова ScriptCiTy. Пример города представлен на
рис. 2.109.
В этом разделе мы рассмотрели масштабные варианты генерации миров, но ты
можешь потренироваться на малых локациях.
РИС. 2.109. КАРТА ГОРОДА
ЗАДАНИЕ
Добавь в разных местах игры возможность генерации яблока, позволяющего
персонажу ускоряться.
Сгенерируй с помощью таблицы локацию «Лабиринт».
122
РАЗРАБОТКА СЛОЖНЫХ СТРУКТУР
ИГРОВЫХ ОБЪЕКТОВ И ЛОКАЦИЙ
В этом разделе углубимся в разработку игровых объектов со сложной структурой.
Их можно создавать с помощью встроенного инструментария Roblox Studio, например через инструмент Solid Modeling.
Создание сложного игрового объекта с использованием этого инструмента
основывается на булевой алгебре. Оператор Union объединяет в одно целое все
выделенные части и присваивает им материал первого выделенного объекта.
На рис. 2.110 показан вариант двух игровых объектов, состоящих из зеленой
сферы и красного параллелепипеда. Они были созданы с помощью оператора
Union. В зависимости от того, какой объект был выбран первым, двум остальным
присваивается материал первого объекта. В этом примере у параллелепипеда
изначально был материал Plastic, а у шара — Metal.
РИС. 2.110. ДВА ОБЪЕКТА, СОСТОЯЩИЕ ИЗ ДВУХ ДЕТАЛЕЙ
Материал первого объекта — металл, второго — пластик. Если включить параметр их свойства UserPartColor, то связанные детали примут цвет первой детали
этого объекта (рис. 2.111).
Такие объекты неразрывны. Их можно создавать, не только объединяя две и более
детали, но и вырезая одну деталь из другой. Тонкости моделирования с помощью
скрипта и вручную описаны в книге «Roblox. Играй, программируй и создавай
свои миры». Здесь же я покажу только некоторые варианты.
Для игр нам понадобятся не монолитные однотонные конструкции из одного
материала, а подвижные, из разных материалов и разных цветов. Например,
автомобиль из проекта dumage_health.rbxl. Этот автомобиль — игровая модель,
созданная из деталей и игровых объектов. Все эти элементы сгруппированы
в одну конструкцию — модель.
123
РИС. 2.111. ПРИСВАИВАНИЕ ЦВЕТА ПЕРВОЙ ДЕТАЛИ ОБЪЕКТА
В качестве примера создадим модель трехэтажного многоквартирного дома
с именем house и сделаем на его основе целую улицу.
У дома есть отдельные элементы, которые нельзя объединять с помощью Union, —
стеклянные окна, рамы, двери, дверные ручки, ступеньки и т. д. Такие части группируют в модель. Для начала создадим Part, размером (100, 50, 40). Затем создадим
три Part с размером (7, 10, 10) и расположим их друг под другом на расстоянии
3–4 studs. Вставим их в места расположения потенциальных окон (рис. 2.112).
РИС. 2.112. СОЗДАНИЕ ОТВЕРСТИЙ ПОД ОКНА
124
Теперь объединим три одинаковых Part с помощью Union (рис. 2.113).
РИС. 2.113. СОЗДАНИЕ ОБЪЕКТА UNION
Затем клонируем эти объекты и равномерно расположим их вдоль всей стены,
оставив место для входа в дом. Настроив положение, добавим клоны окон по
бокам (рис. 2.114).
РИС. 2.114. НАСТРОЙКА ПОЛОЖЕНИЯ ОКОН
Настроив две боковые стороны, перейдем к оставшимся сторонам. У второй
большой боковой стороны не будет входа, поэтому поставим еще один ряд окон.
Затем объединим все элементы окон на каждой стороне с помощью Union так,
чтобы получилось четыре объекта с именем Union (рис. 2.115).
125
РИС. 2.115. ЧЕТЫРЕ МАССИВА ПОД ОКНА ДЛЯ ДОМА
Этими массивами мы будем вырезать в здании отверстия под окна и на их место
ставить стеклянные элементы. Чтобы не создавать такие же массивы из стекла
вручную, клонируем каждый массив. Уменьшим толщину у клона: в моем случае по z до 0.5 studs, и назовем клоны Windows0, Windows1, Windows2 и Windows3
(рис. 2.116).
РИС. 2.116. МАССИВЫ ОКОН
126
Каждый Union преобразуем в Negate с помощью одноименного инструмента из
Solid Modeling (рис. 2.117).
РИС. 2.117. ПОДГОТОВКА ЭЛЕМЕНТОВ ПОД ВЫРЕЗАНИЕ
Осталось вырезать отверстия для каждой стороны дома. Удерживая Ctrl, выбираем
дом и все NegativePart, после чего используем оператор Union. Результат должен
быть таким, как на рис. 2.118.
РИС. 2.118. ВЫРЕЗАННЫЕ ОТВЕРСТИЯ ПОД ОКНА
127
Теперь пристроим к отверстиям объекты Windows. Выберем для них голубоватый цвет, зададим материал Glass и укажем прозрачность в значении 0.75–0.8
(рис. 2.119). Предварительно установим галочку у параметра UserPartColor.
РИС. 2.119. ДОМ С ОКНАМИ
Создадим лестницу с тремя ступеньками: нижняя ступенька будет иметь размер
(17, 2, 10), а верхняя (17, 2, 5). Объединим три ступеньки лестницы и изменим
параметр CollisionFidelity на значение PresizeConvexDecomposition. Это нужно для
того, чтобы взбираться по этой детали как по лестнице (рис. 2.120).
РИС. 2.120. НАСТРОЙКА ЛЕСТНИЦЫ
128
Теперь создадим элемент, который будет вырезать вход в дом, и дверь с ручкой.
Размеры первого элемента (5, 8, 5), размеры двери — (5, 8, 0.3). Приварим ручку
к двери (рис. 2.121).
РИС. 2.121. ЗАГОТОВКА ДЛЯ ДВЕРИ
Осталось вырезать отверстие для двери и поставить ее туда. Чтобы ручка и дверь
перемещались вместе, перенесем ручку внутрь элемента двери, тогда дверь станет
родителем ручки (рис. 2.122).
РИС. 2.122. ИЕРАРХИЯ ИГРОВОГО ОБЪЕКТА
129
Выделим все элементы дома и воспользуемся оператором Group As a Model
(рис. 2.123).
РИС. 2.123. СОЗДАЕМ МОДЕЛЬ
Он объединит все элементы в группу и назовет их общим элементом Model. Элементы группы никак не связаны друг с другом, кроме группировки. Но воздействуя теперь на модель, мы воздействуем и на все ее элементы. Назовем модель
House, а ключевую часть здания Home (рис. 2.124).
РИС. 2.124. МОДЕЛЬ ДОМА
У модели должна быть главная часть, относительно которой все остальные ориентируются. Эта часть должна быть добавлена в параметр PrimaryPart модели.
Таким элементом станет Home. Чтобы ни одна часть модели при тестировании
не падала, поставим всем Achor = true через модель. Щелкни по модели и активируй этот параметр в верхнем меню Roblox Studio (рис. 2.125).
130
РИС. 2.125. НАСТРОЙКА МОДЕЛИ ДОМА
Осталось поработать над визуальной частью дома: подобрать к каждому элементу
модели нужный материал и цвет. Пример настройки представлен на рис. 2.126.
РИС. 2.126. МОДЕЛЬ ДОМА С НАСТРОЙКАМИ ЦВЕТА И МАТЕРИАЛА
Добавим модели элемент интерактивности. Дверь будет открываться по нажатии
левой кнопки мыши с помощью класса ClickDetector, который мы добавим. Чтобы
она открывалась корректно, изменим положение опорной точки (рис. 2.127).
131
РИС. 2.127. НАСТРОЙКА ДВЕРИ ДЛЯ ОТКРЫТИЯ
Добавим к элементу Door скрипт OpenScript.
Напишем код для открытия двери, здесь используем знания, полученные из
предыдущих разделов книги.
local Pivot = script.Parent:GetPivot()
function rot()
--открытие двери
for i =0, 90 do
script.Parent:PivotTo(Pivot * CFrame.Angles(0,math.rad(i), 0))
wait(0.01)
end
wait(3)
--закрытие двери
for i =90, 0, -1 do
script.Parent:PivotTo(Pivot * CFrame.Angles(0,math.rad(i), 0))
wait(0.01)
end
end
-- событие нажатия мыши по двери
script.Parent.ClickDetector.MouseClick:Connect(rot)
Этот пример показывает, как можно создавать различные модели. И сейчас на
основе этой модели мы построим улицу.
Элемент Home имеет размеры (100, 50, 40): это нужно учитывать, распределяя
модели в пространстве. При перемещении модели можно либо связать элементы
Weld, либо сделать Home родителем остальных объектов модели, например, как
на рис. 2.128.
132
РИС. 2.128. СТРУКТУРА МОДЕЛИ ДЛЯ КОРРЕКТНОГО ПЕРЕМЕЩЕНИЯ
Перенесем модель в папку ReplicatedStorage, где создадим ее клоны. Скрипт, который будет строить улицу, добавим в папку Workspace.
Теперь создадим таблицу, где распределим числа от 0 до 4. Значение 0 — это
пустое пространство, 1 — исходная ориентация дома, 2 — дом, повернутый на 90
градусов против часовой стрелки, 3 — дом, повернутый на 90 градусов по часовой
стрелке, 4 — дом, повернутый на 180 градусов. Затем создадим два вложенных
цикла и пройдемся по таблице. В зависимости от выбранного значения будет
создаваться клон здания с определенной ориентацией.
-- схема улицы
local streetTab =
{{2,1,1,1,1,1,0,3},
{0,0,0,0,0,0,0,3},
{2,0,1,1,0,1,0,3},
{2,0,0,0,0,2,0,3},
{2,0,4,0,4,4,0,3},
{2,0,0,0,0,0,0,3},
{2,1,0,1,1,1,0,3},
{2,4,0,4,4,4,0,3}}
-- исходный дом
local bilding = game.ReplicatedStorage.House
-- индекс для ориентации дома
local H
-- процесс построения улицы
for i =1, 8 do
for j =1, 8 do
-- определяем значение из таблицы
H = streetTab[i][j]
-- если H не ноль, то строим
if H~=0 then
local cloneBild = bilding:Clone()
cloneBild.Name ="H"..tostring(j)..tostring(i)
if H ==1 then
cloneBild.PrimaryPart:PivotTo(CFrame.new(j*120, 25, i*120)*
CFrame.Angles(0,0,0))
elseif H ==2 then
cloneBild.PrimaryPart:PivotTo(CFrame.new(j*120, 25, i*120)*
CFrame.Angles(0,math.rad(90),0))
elseif H ==3 then
133
cloneBild.PrimaryPart:PivotTo(CFrame.new(j*120, 25, i*120)*
CFrame.Angles(0,math.rad(-90),0))
end
end
elseif H ==4 then
cloneBild.PrimaryPart:PivotTo(CFrame.new(j*120, 25, i*120)*
CFrame.Angles(0,math.rad(180),0))
end
cloneBild.Parent = workspace
end
По осям x и z между зданиями сделаны дополнительные отступы на 20 и на
40 studs, чтобы здания смотрелись органично. При запуске все должно выглядеть
так, как на рис. 2.129.
РИС. 2.129. УЛИЦА ИЗ МОДЕЛЕЙ
Теперь на эту улицу можно добавить и другие варианты моделей: светофоры,
фонарные столбы, деревья и т. д.
ЗАДАНИЕ
Создай для улицы модель работающего светофора и пешеходного перехода.
День может сменяться ночью, поэтому добавь модели фонарных столбов.
Сделай так, чтобы на них включалось освещение, когда стемнеет.
Создай модель магазина.
134
ИМПОРТ 3D-МОДЕЛЕЙ, АНИМАЦИИ
И ТЕКСТУР
Мы уже умеем создавать и сложные объекты, и модели. В дальнейшим я продолжу
показывать примеры построения таких элементов для игры.
Но иногда требуется добавить модель, которую с помощью Roblox Studio делать
сложно или долго. Или нужна сложная анимация с захватом движений реального
человека или животного. Тогда на помощь приходит возможность импортирования 3D-модели и анимации.
В Roblox Studio определены ограничения на количество полигонов для 3D-модели:
они не должны превышать 10 000. Импортируемая 3D-модель может быть в формате .fbx или .obj. Формат .obj 3D-модели может некорректно отображаться при
загрузке, поэтому рекомендую расширение .fbx.
Импортировать 3D-модель можно двумя способами: загрузить через MeshPart
или через Avatar Importer. В первом варианте 3D-объект загружается как целостный элемент, а во втором — как модель, если изначально были элементы, не
связанные друг с другом. В последнем варианте также добавляются элементы
для анимации, поскольку этот способ больше предназначен для 3D-моделей со
скелетом и готовой анимацией. Кроме того, он помогает обойти ограничение
в 10 000 полигонов.
В качестве примера я покажу, как загрузить модель, созданную в редакторе
Blender. Модель представляет собой старинный замок, состоящий из отдельных
частей (рис. 2.130).
РИС. 2.130. 3D-МОДЕЛЬ ЗАМКА В BLENDER
135
Общая сумма полигонов превышает 10 000, поэтому я загружу модель по частям,
экспортируя их в формате .fbx (рис. 2.131).
РИС. 2.131. ЭКСПОРТ В ФОРМАТ FBX
Всего получилось шесть частей: центральная башня, боковая башня, Т-образная
конструкция, две башни сбоку, шипастый тор и купол с четырьмя башнями.
Для загрузки шести частей нужно добавить на сцену шесть MeshPart и в параметре
MeshId прописать путь к 3D-модели части замка (рис. 2.132).
Теперь осталось собрать эти детали в модель или объединить в Union, предварительно добавив цвет и материал.
Подобную задачу можно проделать и вторым способом, для чего не нужно экспортировать модель по частям. Достаточно полностью преобразовать ее в формат
.fbx и через этот плагин подгрузить в свой проект. Плагин предложит выбрать
перед этим форму объекта, так как подразумевается, что мы загружаем анимированную модель. На этом шаге выбираем Custom. Результат загрузки модели
целиком представлен на рис. 2.133.
136
РИС. 2.132. 3D-МОДЕЛЬ ИЗ ИМПОРТИРУЕМЫХ ЧАСТЕЙ
РИС. 2.133. ВТОРОЙ ВАРИАНТ ЗАГРУЗКИ 3D-МОДЕЛИ ЧЕРЕЗ AVATAR IMPORTER
В окне Explorer мы видим, что замок уже стал моделью: осталось добавить материал и цвет.
Пример дизайна модели представлен на рис. 2.134.
137
РИС. 2.134. МОДЕЛИ ЗАМКОВ
При публикации локации с импортированными моделями Mesh будут автоматически загружены на сервер. Ты сможешь всегда получить к ним доступ через
Toolbox (рис. 2.135) или в личном кабинете на странице Roblox Create.
РИС. 2.135. ЗАГРУЖЕННЫЕ MESH НА СЕРВЕР ROBLOX
Созданную или загруженную модель можно опубликовать. Для этого щелкни
правой кнопкой мыши по модели в окне Explorer и выбери Save to Roblox. Процесс
сохранения модели схож с процессом публикации игровой локации. Если модель
опубликована, то будет доступна в твоем инвентаре в Toolbox во вкладке Models.
138
РИС. 2.136. ОПУБЛИКОВАННАЯ МОДЕЛЬ
Теперь загрузим модель с анимацией. Для этого я возьму модель мужчины, которую сделал в программе Fuse. Можно подгрузить и модель из готовых (рис. 2.137).
РИС. 2.137. СОЗДАНИЕ ПЕРСОНАЖА В ПРОГРАММЕ FUSE
После создания персонажа я добавлю ему анимацию через сайт Mixamo. Также
анимацию можно создать самим, например с помощью Blender. Для работы
в Mixamo формат персонажа должен быть .obj или .fbx. Ниже я расскажу о работе
с Mixamo.
139
Откроем сайт Mixamo и подгрузим нашу модель с помощью кнопки Upload
Character (рис. 2.138).
РИС. 2.138. ДОБАВЛЕНИЕ ГУМАНОИДА В MIXAMO
После подтверждения выбора модели нужно настроить ключевые узлы для скелета, чтобы анимация наложилась корректно (рис. 2.139).
РИС. 2.139. НАСТРАИВАЕМ УЗЛЫ ДЛЯ АНИМАЦИИ
Нажимаем Next и ждем, когда закончатся вычисления. Затем найдем нужную анимацию и посмотрим, как она будет выглядеть для нашего персонажа (рис. 2.140).
140
РИС. 2.140. ВЫБОР АНИМАЦИИ ДЛЯ ПЕРСОНАЖА
Ставим галочку на параметре In Place, чтобы анимация происходила на одном
месте. Если анимация понравилась, скачиваем ее в формате .fbx вместе с моделью.
Если нужна другая анимация, то повторяем все то же самое: ищем, накладываем
и скачиваем (рис. 2.141).
РИС. 2.141. СКАЧИВАЕМ ВЫБРАННУЮ АНИМАЦИЮ
141
Названия скачанных файлов будут такие же, как и названия использованной
анимации.
Вернемся в Roblox Studio и с помощью плагина Avatar Importer загрузим нашу
анимированную модель, применив вариант Custom. Затем запустим плагин
Animation Editor.
Щелкнем по модели и назовем анимацию. Затем загрузим ее с помощью инструмента import. Импортировать нужно тот же файл в формате .fbx, который мы
скачали ранее (рис. 2.142).
Теперь осталось проверить работу анимации. Если все работает корректно, то
сохраним анимацию и опубликуем ее. Если нужно подгрузить еще одну анимацию, то повторяем процедуру импорта из другого файла .fbx. Теперь напишем
скрипт, который будет по очереди запускать первую, а затем вторую анимацию.
Добавим в нашу модель скрипт, в котором будут два элемента Animation. В нашем
примере есть анимация остановки персонажа и анимация движения. Назовем
их Stop и Move.
РИС. 2.142. ЗАГРУЖАЕМ АНИМАЦИЮ ИЗ ФАЙЛА .FBX
142
Затем перейдем в свойства элемента Animation и для параметра AnimationId добавим
ID опубликованных анимаций (рис. 2.143).
РИС. 2.143. ДОБАВЛЯЕМ ОПУБЛИКОВАННЫЕ АНИМАЦИИ
Осталось прописать логику в скрипте:
-- находим главный элемент по включению анимации
local player = script.Parent:WaitForChild("AnimationController")
-- определяем анимации
local animStop =script.Stop
local animMove = script.Move
-- подгружаем контроллер анимации
local animStop1 = player:LoadAnimation(animStop)
local animMove1 = player:LoadAnimation(animMove)
--определяем свойство для анимации на зацикливание
animStop1.Looped = true
animMove1.Looped = true
-- бесконечный цикл на поочередный запуск анимации
while wait(0.1) do
animStop1:Play()
animMove1:Stop()
wait(5)
animStop1:Stop()
animMove1:Play()
wait(5)
end
Так как мы использовали модель шаблона Custom, то в модели нет элемента
Humanoid, но есть элемент AnimationController, который выполняет ту же функцию.
Согласно скрипту анимации будут сменяться каждые 5 секунд.
143
Кроме того, каждый элемент модели можно окрасить и подключить текстуры
из Toolbox.
ЗАДАНИЯ
Загрузи готовую или созданную самостоятельно 3D-модель автомобиля в проект Roblox Studio.
Найди готовую анимацию или самостоятельно создай в стороннем редакторе
анимацию 3D-модели и импортируй ее в Roblox Studio.
ЭКОНОМИКА В ИГРАХ: ИГРОВЫЕ
ТОВАРЫ, ПОКУПКА, ИНВЕНТАРЬ
В предыдущих разделах мы уже познакомились с покупкой товаров на примере
сбора монет и покупки меча. По этому принципу работает и внутриигровая валюта. Ты можешь сам придумать денежные средства, которые будут использовать
игроки в твоем проекте.
Часто разработчик закладывает в проекты либо полноценную экономическую
модель игрового пространства, либо какую-то ее часть. Например, это может
быть экономическая игра, где персонажу нужно работать, чтобы зарабатывать
деньги, покупать недвижимость, оплачивать продукты или коммунальные услуги. Также это может быть симулятором города или страны, где нужно создавать
производственные и экономические институты для роста и налаживать связи
с соседними городами или странами.
Игрок может зарабатывать игровые деньги, выполняя задания, чтобы затем покупать новый опыт, инструменты и технологии, которые дадут ему привилегии
перед другими или позволят перейти на определенный уровень игры.
Товары могут быть разного назначения: например, для повышения (восстановления) здоровья персонажа, для прохождения квестов или помогающие быстро
выполнить ряд игровых задач.
Купленные товары можно вывести перед игроком (появление), разместить
в рюкзаке персонажа (как инструмент), сразу улучшить характеристику,
которая может отображаться в GUI, или разместить в инвентаре, созданном
с помощью GUI.
В качестве примера создадим магазин, где персонаж может купить нужный
товар. Рассмотрим три варианта получения этих товаров. В магазине будет использоваться валюта dinar. Создадим модель, представляющую собой торговую
лавку (рис. 2.144).
144
РИС. 2.144. ТОРГОВАЯ ЛАВКА
Сделаем вывеску со словом «Магазин». Для этого я воспользуюсь 3D-редактором
Blender и создам объемный текст, который экспортирую в качестве 3D-модели
в формате .fbx (рис. 2.145). Для этих целей можно использовать любой другой
3D-редактор, который может экспортировать модель в нужные для Roblox форматы (см. раздел «Импорт 3D-моделей, анимации и текстур»). Вывеску можно
сделать с помощью плагина по созданию текста, однако не все плагины поддерживают русский язык и не всегда работают корректно.
РИС. 2.145. 3D-МОДЕЛЬ ВЫВЕСКИ В BLENDER
После создания файла формата .fbx вызываем MeshPart и присваиваем ему эту
модель. Добавляем цвет и вставляем в модель магазина (рис. 2.146).
145
РИС. 2.146. МОДЕЛЬ МАГАЗИНА С ВЫВЕСКОЙ
Теперь разместим образцы товаров. Я использую свои модели: яблоко, гриб
и аптечку (рис. 2.147).
РИС. 2.147. МОДЕЛИ ТОВАРОВ
Когда модели статичны, то меньше привлекают внимание потенциальных покупателей. Добавим эффекты вращения и цвета. Яблоко и аптечка будут вращаться
на месте, а гриб будет заключен в полупрозрачный бокс, мерцающий разными
цветами. Каждой модели добавим ClickDetector, бокс назовем Fon и снимем свойства CanCollide, CanQuery и CanTouch. Добавим всем трем предметам и Fon элемент
Script (рис. 2.148).
146
РИС. 2.148. НАСТРОЙКА ЭФФЕКТОВ ДЛЯ ТОВАРОВ
Напишем алгоритм вращения яблока и аптечки: они одинаковы за исключением
того, что аптечка — это игровой целостный объект, а яблоко — игровая модель.
За основу алгоритма возьмем код для монеты из раздела «Игровой опыт».
Пример кода для яблока:
local runServer = game:GetService("RunService")
local Pivot = script.Parent.PrimaryPart:GetPivot()
local angle = 0
function rot()
script.Parent.PrimaryPart:PivotTo(Pivot * CFrame.Angles(0,math.rad(angle), 0))
angle +=3
if angle >=360 then
angle =0
end
end
runServer.Stepped:Connect(rot)
Пример кода для аптечки:
local runServer = game:GetService("RunService")
local Pivot = script.Parent:GetPivot()
local angle = 0
function rot()
script.Parent:PivotTo(Pivot * CFrame.Angles(0,math.rad(angle), 0))
angle +=3
if angle >=360 then
angle =0
end
end
runServer.Stepped:Connect(rot)
147
Теперь перейдем к коду для бокса Fon. Здесь я случайным образом буду изменять
цвет коробки:
local r,g,b
while wait(0.5) do
r = math.random()
g = math.random()
b = math.random()
script.Parent.Color = Color3.new(r,g,b)
end
Теперь зайди в игру: ты увидишь, что яблоко и аптечка стали вращаться, а вокруг
гриба добавилось мерцание.
Для вызова купленных предметов нужно вызывать их клоны. Чтобы их можно
было собрать, создадим скрипт на уничтожение при касании (эффект сбора).
Пример такого скрипта см. в разделе «Сбор, разрушение, ремонт, лечение и нанесение урона».
Код для яблока, аптечки и гриба:
function apple_money(player)
if player and player.Parent:FindFirstChild("Humanoid") then
end
script.Parent:Destroy()
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
Разместим клоны товаров в ReplicatedStorage и добавим каждому из них скрипт,
приведенный выше (рис. 2.149).
РИС. 2.149. КЛОНЫ ВЫЗЫВАЕМЫХ ОБЪЕКТОВ
148
Теперь допишем в эти скрипты вызов клонов при клике по товарам (тем, что
вращаются и мерцают). Клоны разместим рядом с лавкой. Для этого либо заранее
определим ее координаты, либо определим их уже в скрипте.
Я воспользуюсь вторым способом и определю координаты PrimaryPart магазина.
В моем случае это координаты стола лавки. Определив координаты, я клонирую
объекты со сдвигом по одной из осей, например по Z. Допишем код в уже имеющийся скрипт.
Пример кода для яблока:
-- координаты лавки
local Pos = game.Workspace.Market.PrimaryPart.Position
--создание клона яблока
function create()
local appleClone = game.ReplicatedStorage.apple:Clone()
appleClone.PrimaryPart.Position = Pos+Vector3.new(0,0,10)
appleClone.Parent = workspace
end
script.Parent.ClickDetector.MouseClick:Connect(create)
Аналогично добавляем код для гриба и аптечки. Содержание будет почти таким
же, за исключением создания клона. Опираясь на раздел «Игровой опыт», создадим глобальную переменную IntValue с именем Dinar, которая будет хранить
сумму денег игрока.
Не будем повторять содержание того раздела, где достижения нужно хранить
на сервере: здесь достаточно добавить вариант вычитания нужной суммы при
покупке. Для визуализации добавим простой GUI (рис. 2.150).
РИС. 2.150. GUI С МОНЕТАМИ
149
Для начала укажем значение Dinar, равное 100. Теперь нужно сделать так, чтобы
при клике по выбранным предметам на лавке из этой переменной вычиталась
стоимость товара, а в GUI отображался текущий остаток (TextLabel). Для корректной и правильной работы с таким вариантом передачи данных используется
класс Remote Events.
Документация: https://create.roblox.com/docs/reference/engine/classes/RemoteEvent.
Задача этого класса — настраивать связь между локальными данными на твоем
компьютере или мобильном устройстве с данными на сервере Roblox. GUI, как
и сам загруженный игрок, — это локальные данные. Мы можем изменять значения внутри игры, но только локально. А локальные данные не могут просто так
передаваться в этой среде: многим нужно получать данные с сервера (которые
передали локальные игроки или события).
Чтобы синхронизировать процесс передачи с сервера на клиент данные с Dinar
в TextLabel, воспользуемся элементом RemoteEvent, который разместим в папке
ReplicatedStorage (рис. 2.151).
РИС. 2.151. ДОБАВЛЕНИЕ REMOTEEVENT
Создадим LocalScript для TextLabel, где отображаются сбережения персонажа. Мы
будем подключаться к RemoteEvent и присваивать значение в текстовом поле
с сервера при изменении значения.
local remote = game.ReplicatedStorage.RemoteEvent
remote.OnClientEvent: Connect (function (money)
script.Parent.Text = tostring(money)
end)
150
Теперь обработаем это событие при самом списании, то есть при клике по яблоку, грибу и аптечке. Это значит, что всем товарам в их скриптах нужно добавить
код обработки события. Здесь мы вызываем функцию FireClient, где передаем имя
игрока и то, что передаем, — в нашем случае значение Dinar. В каждом из этих
скриптов (где обрабатывается вращение и мерцание) добавляем строку
local remote = game.ReplicatedStorage.RemoteEvent
и исправляем функцию create. Ниже представлен исправленный код для яблока:
function create(player)
print(player)
-- вычитаем стоимость
game.Workspace.Dinar.Value -=5
--передаем клиенту
remote:FireClient(player,game.Workspace.Dinar.Value)
local appleClone = game.ReplicatedStorage.apple:Clone()
appleClone.PrimaryPart.Position = Pos+Vector3.new(0,0,10)
appleClone.Parent = workspace
end
script.Parent.ClickDetector.MouseClick:Connect(create)
Исправляем функцию create для аптечки и гриба: для проверки рекомендую
изменить стоимость списания. Гриб стоит 10 динаров, а аптечка — 15. Пример
кода для гриба:
local remote = game.ReplicatedStorage.RemoteEvent
local Pos = game.Workspace.Market.PrimaryPart.Position
function create(player)
print(player)
-- вычитаем стоимость
game.Workspace.Dinar.Value -=10
--передаем клиенту
remote:FireClient(player,game.Workspace.Dinar.Value)
local appleClone = game.ReplicatedStorage.mushroom_white:Clone()
appleClone.PrimaryPart.Position = Pos+Vector3.new(0,0,10)
appleClone.Parent = workspace
end
script.Parent.ClickDetector.MouseClick:Connect(create)
Ты можешь разместить каждую модель, которая расположена в ReplicatedStorage,
как элемент tool. Переименуем этот инструмент по названиям моделей и изменим
в коде его расположение — отправим в рюкзак игрока player.Backpack. Теперь
после покупки товар появляется в рюкзаке и его можно активировать.
Пример кода для яблока, который вызывает инструмент apple в рюкзак.
function create(player)
print(player)
-- вычитаем стоимость
game.Workspace.Dinar.Value -=5
--передаем клиенту
remote:FireClient(player,game.Workspace.Dinar.Value)
local appleClone = game.ReplicatedStorage.apple:Clone()
151
-- размещаем в рюкзаке персонажа
appleClone.Parent = player.Backpack
end
script.Parent.ClickDetector.MouseClick:Connect(create)
Есть разные способы работы с удаленными событиями через клиент — сервер
и, наоборот, сервер — клиент. В таблице ниже представлены эти способы.
Удаленные события
Клиент → Сервер
Клиент
RemoteEvent: FireServer (аргументы)
Сервер
RemoteEvent.OnServerEvent: Connect (функция (игрок, аргументы))
Сервер → Клиент
Сервер
RemoteEvent:FireClient (игрок, аргументы)
Клиент
RemoteEvent.OnClientEvent: Connect (функция (аргументы))
Сервер → Все клиенты
Сервер
RemoteEvent:FireAllClients (аргументы)
Клиент
RemoteEvent.OnClientEvent: Connect (функция (аргументы))
Удаленные функции
Клиент → Сервер → Клиент
Клиент
serverResponse = RemoteFunction:InvokeServer (аргументы)
Сервер
RemoteFunction.OnServerInvoke: Connect (функция (игрок, аргументы))
Купленный или собранный предмет можно разместить в своем инвентаре.
Попробуем создать пример простого инвентаря, где будут отображаться все
купленные товары. Сначала создадим Frame в ScreenGui — это рамка, которая будет
группировать в своей области другие GUI-объекты. Сделаем шесть ImageButton
и TextLabel (рис. 2.152).
РИС. 2.152. ИНВЕНТАРЬ
152
Здесь будет предложен вариант упрощенного инвентаря. Каждая ячейка зарезервирована под определенный товар в игре. При покупке товара предмет
отобразится в указанной для него ячейке, а ниже будет показано количество.
Следующий шаг настройки инвентаря — его отображение по нажатии клавиши F.
В разделе «Сбор» я рассматривал пример реализации использования клавиши. Для
работы с клавиатурой нам снова понадобится LocalScript. Графический инвентарь
также отображается для конкретного игрового персонажа и поэтому относится
к клиенту. Нужно передавать команды на сервер от пользователя, чтобы оттуда
передавать их на пользовательский инвентарь. Индикатором включения и отключения инвентаря станет глобальная переменная IntValue с именем N. Если ее
значение будет равно нулю, то инвентарь отображаться не будет, а если единице — будет. Разместим эту переменную в Workspace.
Нам снова понадобится скрипт RemoteEvent , который мы переименуем
в RemoteInventar. Затем перейдем в папку StarterPlayerScripts и создадим LocalScript,
где и пропишем работу с клавишей F.
local ContextActionService = game:GetService("ContextActionService")
local remote = game.ReplicatedStorage.RemoteInventar
local function handleAction(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
if actionName == "Key" then
game.Workspace.N.Value +=1
end
end
end
if game.Workspace.N.Value > 1 then
game.Workspace.N.Value = 0
end
-- передаем с клиента на сервер значение
remote: FireServer (game.Workspace.N.Value)
ContextActionService:BindAction("Key", handleAction, true, Enum.KeyCode.F)
Теперь нужно получить данные на сервере и присвоить значение нашей переменной N. Для этого создадим скрипт в ServerScriptService, где будем присваивать
переменной N полученное от клиента значение. В этом же скрипте будем отправлять на клиент значение true или false в зависимости от значения N.
local remote = game.ReplicatedStorage.RemoteInventar
remote.OnServerEvent: Connect (function (player, N)
-- получаем значение от игрока и присваиваем его N
game.Workspace.N.Value = N
-- отправляем клиенту true и false
if game.Workspace.N.Value == 0 then
remote:FireClient(player, false)
else
remote:FireClient(player, true)
end
end)
153
Теперь эти данные должен прочитать LocalScript , расположенный во Frame
(рис. 2.153).
local remote = game.ReplicatedStorage.RemoteInventar
remote.OnClientEvent: Connect (function (N)
script.Parent.Visible = N
end)
Теперь при нажатии клавиши F на экране будет появляться и исчезать инвентарь.
Дальше нужно сделать так, чтобы при клике по товару (покупка) он отобразился
в инвентаре с указанием количества.
Для начала создадим три RemoteEvent для каждого типа товара и назовем их Remo
teApple, RemoteMush и RemoteApteca. Разместим их в ReplicatedStorage (рис. 2.153).
РИС. 2.153. СОЗДАНИЕ УДАЛЕННЫХ СОБЫТИЙ
Теперь создадим три глобальные переменные IntValue и разместим их в Workspace
с одноименными названиями: AppleNum, MushNum, AptecNum (рис. 2.154).
РИС. 2.154. ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
154
Допишем код, который будет передавать данные с сервера на клиент в скрипт,
расположенный в модели яблока. Полный код для яблока:
local remote = game.ReplicatedStorage.RemoteEvent
local rem = game.ReplicatedStorage.RemoteApple
local runServer = game:GetService("RunService")
local Pivot = script.Parent.PrimaryPart:GetPivot()
local angle = 0
function rot()
script.Parent.PrimaryPart:PivotTo(Pivot * CFrame.Angles(0,math.rad(angle), 0))
angle +=3
if angle >=360 then
angle =0
end
end
runServer.Stepped:Connect(rot)
local Pos = game.Workspace.Market.PrimaryPart.Position
function create(player)
print(player)
-- вычитаем стоимость
game.Workspace.Dinar.Value -=5
--передаем клиенту
remote:FireClient(player,game.Workspace.Dinar.Value)
-- увеличиваем значение яблока и передаем клиенту
game.Workspace.AppleNum.Value+=1
rem:FireClient(player, game.Workspace.AppleNum.Value)
-- создание клона яблока
local appleClone = game.ReplicatedStorage.apple:Clone()
appleClone.PrimaryPart.Position = Pos+Vector3.new(0,0,10)
appleClone.Parent = workspace
end
script.Parent.ClickDetector.MouseClick:Connect(create)
Добавляем код для гриба:
local remote = game.ReplicatedStorage.RemoteEvent
local rem = game.ReplicatedStorage.RemoteMush
local Pos = game.Workspace.Market.PrimaryPart.Position
function create(player)
print(player)
-- вычитаем стоимость
game.Workspace.Dinar.Value -=10
--передаем клиенту
remote:FireClient(player,game.Workspace.Dinar.Value)
-- увеличиваем значение гриба и передаем клиенту
game.Workspace.MushNum.Value+=1
rem:FireClient(player, game.Workspace.MushNum.Value)
local appleClone = game.ReplicatedStorage.mushroom_white:Clone()
appleClone.PrimaryPart.Position = Pos+Vector3.new(0,0,10)
appleClone.Parent = workspace
end
script.Parent.ClickDetector.MouseClick:Connect(create)
155
Код для скрипта аптечки:
local remote = game.ReplicatedStorage.RemoteEvent
local rem = game.ReplicatedStorage.RemoteApteca
local runServer = game:GetService("RunService")
local Pivot = script.Parent:GetPivot()
local angle = 0
function rot()
script.Parent:PivotTo(Pivot * CFrame.Angles(0,math.rad(angle), 0))
angle +=3
if angle >=360 then
angle =0
end
end
runServer.Stepped:Connect(rot)
local Pos = game.Workspace.Market.PrimaryPart.Position
function create(player)
-- вычитаем стоимость
game.Workspace.Dinar.Value -=15
remote:FireClient(player,game.Workspace.Dinar.Value)
-- увеличиваем значение аптечки и передаем клиенту
game.Workspace.AptecNum.Value+=1
rem:FireClient(player, game.Workspace.AptecNum.Value)
local appleClone = game.ReplicatedStorage.apteca:Clone()
appleClone.Position = Pos+Vector3.new(0,0,10)
appleClone.Parent = workspace
end
script.Parent.ClickDetector.MouseClick:Connect(create)
Теперь осталось прописать получение событий с сервера клиенту в LocalScript
у Frame.
local remote = game.ReplicatedStorage.RemoteInventar
local rem = game.ReplicatedStorage.RemoteApple
local rem1 = game.ReplicatedStorage.RemoteMush
local rem2 = game.ReplicatedStorage.RemoteApteca
-- включение и отключение инвентаря
remote.OnClientEvent: Connect (function (N)
script.Parent.Visible=N
end)
-- добавление яблок
rem.OnClientEvent: Connect (function (apple)
script.Parent.ImageApple.Image = "rbxassetid://11527074832"
script.Parent.ImageApple.TextLabel.Text =tostring(apple)
end)
-- добавление грибов
rem1.OnClientEvent: Connect (function (mush)
script.Parent.ImageMush.Image = "rbxassetid://11527074991"
script.Parent.ImageMush.TextLabel.Text =tostring(mush)
end)
-- добавление аптечки
rem2.OnClientEvent: Connect (function (apteca)
script.Parent.ImageApteca.Image = "rbxassetid://11527075140"
script.Parent.ImageApteca.TextLabel.Text =tostring(apteca)
end)
156
Протестируем игру. Теперь при покупке товаров клоны появляются в заданной
точке, а в инвентаре сразу же отображается купленный предмет и его количество. Игрок может коснуться клонов, и они исчезнут. Последний эффект в нашем случае — избыточная механика, но, возможно, она пригодится тебе потом
(рис. 2.155).
РИС. 2.155. РАЗМЕЩЕНИЕ КУПЛЕННЫХ ТОВАРОВ В ИНВЕНТАРЕ
Попробуем активировать товар из инвентаря. Например, щелкнув по нему, мы
вызываем создание клона и вычитаем количество из инвентаря. Для этого создадим LocalScript для каждой ImageButton. Пример кода для кнопки с яблоками
в инвентаре.
function addApple(player)
local Num = script.Parent.TextLabel.Text
local N = tonumber(Num)
if tonumber(Num)>0 then
local appleClone = game.ReplicatedStorage.apple:Clone()
print(player)
N -=1
script.Parent.TextLabel.Text = tostring(N)
appleClone.Parent=workspace
end
end
script.Parent.MouseButton1Down:Connect(addApple)
Подобный код можно прописать и для других кнопок, чтобы они вызывали предметы из инвентаря. Вызванные предметы на локальном компьютере
будут выглядеть так, как показано на рис. 2.156: это вид от лица игрового
персонажа.
157
РИС. 2.156. СОЗДАНИЕ ПРЕДМЕТОВ ИЗ ИНВЕНТАРЯ
Если мы перейдем в режим сервера, то есть в роль администратора, то картина
уже будет другая, как на рис. 2.157.
РИС. 2.157. ОТОБРАЖЕНИЕ СО СТОРОНЫ СЕРВЕРА
158
На сервере нет этих предметов, а деньги не потрачены. Также видно, что созданные предметы не уничтожаются при касании игроком.
Альтернативный вариант — реализовать процесс по-другому, чтобы вызываемые
из инвентаря предметы отображались для всех игроков в игре. Для этого удалим
из этих локальных файлов коды по созданию и размещению клонов и оставим
только изменение TextLabel. В этих локальных файлах мы будем отправлять события с клиента на сервер. Для этого создадим еще три RemoteEvent в ReplicatedStorage
под именами AddApple, AddApteca и AddMush.
Исправим код в локальных скриптах для кнопок.
Пример для кнопки с яблоками:
local add = game.ReplicatedStorage.AddApple
function addApple(player)
local Num = script.Parent.TextLabel.Text
local N = tonumber(Num)
if tonumber(Num)>0 then
print(player)
N -=1
script.Parent.TextLabel.Text = tostring(N)
-- отправляем с клиента значения о количестве
add:FireServer(N)
end
end
script.Parent.MouseButton1Down:Connect(addApple)
По аналогии со скриптом выше создай локальные скрипты в кнопках гриба и аптечки с использованием их событий AddApteca и AddMush.
Обработка на сервере будет происходить в уже имеющемся скрипте, расположенном в ServerScriptService: он отвечал за отображение инвентаря через сервер.
local
local
local
local
local
remote = game.ReplicatedStorage.RemoteInventar
rem = game.ReplicatedStorage.RemoteApple
add = game.ReplicatedStorage.AddApple
add1 = game.ReplicatedStorage.AddMush
add2 = game.ReplicatedStorage.AddApteca
remote.OnServerEvent: Connect (function (player, N)
game.Workspace.N.Value = N
if game.Workspace.N.Value ==0 then
remote:FireClient(player, false)
else
remote:FireClient(player, true)
end
end)
-- создание на серверной части яблока
159
add.OnServerEvent: Connect (function (player, N)
game.Workspace.AppleNum.Value=N
local appleClone = game.ReplicatedStorage.apple:Clone()
appleClone.Parent=workspace
end)
-- создание на серверной части гриба
add1.OnServerEvent: Connect (function (player, N)
game.Workspace.MushNum.Value=N
local appleClone = game.ReplicatedStorage.mushroom_white:Clone()
appleClone.Parent=workspace
end)
-- создание на серверной части аптечки
add2.OnServerEvent: Connect (function (player, N)
game.Workspace.AptecNum.Value=N
local appleClone = game.ReplicatedStorage.apteca:Clone()
appleClone.Parent=workspace
end)
В этом случае будут отображаться клоны предметов из инвентаря, и значения
глобальных переменных будут синхронизированы со значениями в инвентаре
(рис. 2.158).
РИС. 2.158. ОТОБРАЖЕНИЕ КЛОНОВ ПРЕДМЕТОВ НА СЕРВЕРЕ ПРИ ВЫЗОВЕ ИЗ ИНВЕНТАРЯ
На основе этой механики с удаленными событиями и работой с инвентарем
можно создавать интересные игры.
160
ДОПОЛНЕНИЕ
Если нужно передавать конкретному игроку на клиенте (ПК) с сервера данные
при касании предметов, то можно воспользоваться таким алгоритмом:
local Rem = game.ReplicatedStorage.RemoteEvent
function start(part)
-- получаем данные об игроке, коснувшемся предмета
local Player = game.Players:GetPlayerFromCharacter(part.Parent)
if Player then
-- вывод имени игрока
print(Player)
end
end
script.Parent.Touched:Connect(start)
ЗАДАНИЯ
Создай инвентарь, где можно размещать инструменты.
Сделай модель топора и купи его в игре. Используй его для рубки деревьев —
срубленные бревна должны отображаться в инвентаре.
Сделай элемент старта и финиша. При касании первого начинает отсчитываться время, при касании второго останавливается отсчет. Используй класс os.
На этом базовая часть книги закончена. Следующие главы будут опираться на
информацию, которую мы изучили.
3
ПЛАТФОРМЕР:
СТРУКТУРА
И СОЗДАНИЕ
В этой главе подробно рассмотрим жанр платформера.
Это очень распространенный жанр в игровой индустрии,
который может содержать механики аркады, экшена, квеста
и т. д.
162
ПЛАТФОРМЕР — БЕГУЩИЙ
ПО ЛЕЗВИЮ ИГРЫ
Игры жанра платформер имеют характерные черты — прыжки, переходы на уровни с помощью лестниц (лифтов, батутов), сбор предметов, поиск скрытых мест
с бонусами, избегание многочисленных противников или противодействие им.
Предметы подбираются простым прикосновением. Могут быть бонусы: ускорение, неуязвимость, прыгучесть, броня и т. д. Как правило, игрок перемещается
по линейным трассам 2D- или 3D-формата в горизонтальном или вертикальном
направлении. Трассы могут быть с небольшими ответвлениями, перпендикулярными направлению основного движения.
Кроме противников на пути героя могут быть сложные трассы, а также головоломки, которые нужно решить, чтобы пройти дальше.
В этой главе на примере одной локации (Place) ты узнаешь, как создать базу для
3D-платформера. Разработка игры — это огромное количество информации, которую трудно вписать в объем одной книги, поэтому приходится ограничиваться
отдельными примерами.
Итак, приступим!
ФОРМИРОВАНИЕ ИГРОВОЙ ИСТОРИИ:
МОТИВАЦИЯ ИГРОКА. НАЧАЛО И КОНЕЦ
Первый шаг — выбрать жанр игры, которую будем создавать. С этим мы определились. Теперь нужно написать историю или сценарий.
История (рассказ) — это повествовательное содержание будущей игры.
Например, рассказ о том, как группа археологов раскопала в джунглях древний
артефакт, открывший портал в другое измерение, из которого вылезли древние существа. А потом герои (или герой) останавливают существ и закрывают
портал.
Это краткое описание сюжета игры, но из него мы уже можем получить представление о том, что создавать в игре и какие стилистические элементы подбирать.
История дает общую картину для разработчиков.
Сценарий — это технический документ, где прописываются четкие действия
в игре. У него может быть более точное определение: контекст геймплея.
Здесь описывается, что конкретно происходит в определенный момент и что
произойдет после. Даются описания алгоритма действий каждой сцены игры
и механики.
163
Составить такой документ — трудоемкий процесс, но он облегчает работу с игрой
и взаимодействие с другими разработчиками, работающими над ней. Так возникает меньше вероятности сделать лишнюю и ненужную работу.
В начале своего пути в игровой индустрии рекомендую соблюдать баланс между
историей и сценарием. Например, ты можешь зафиксировать ключевые моменты,
которые должны быть в игре, и прописать, как все должно происходить. Есть два
основных варианта построения сюжетной линии.
Вариант 1:
Завязка.
Конфликт.
Развязка.
Вариант 2:
Завязка.
Конфликт.
Развитие конфликта или появление нового.
Развязка.
В этих пунктах мы конкретно прописываем, что и как должно происходить в нашей игре, а промежуточные варианты оставляем на откуп фантазии.
Первые игры, как правило, небольшие, со средним количеством локаций (актов,
как в пьесе), равным трем. Если ты поработаешь с сюжетом, то всегда можешь
увеличить их число до 10–20 актов. Однако это уже очень большие игры, которые
могут перерасти в долгострой и никогда не выйти в свет. Реально оцени свои
силы и сделай для начала 2–3 небольшие игры. Освой механику разработки,
набери свое портфолио.
Для упрощения я рассмотрю конструкцию, опирающуюся на первый вариант.
Итак, сначала сформируем историю игры.
История. В одной научной лаборатории проводили исследования по выра
щиванию сельскохозяйственных продуктов. Внезапно экспериментальная
установка по синтезу органики дала сбой и начала синтезировать генномодифицированные овощи, фрукты и другие продукты. Они ожили и стали
нападать на людей.
Игрок — один из ученых, работающих в этой организации. Он узнает о событии
в свой выходной. Теперь ему придется пройти сквозь толпу оживших ГМОпродуктов, чтобы выключить машину. Игрок проходит множество опасностей,
мешая ожившим существам, в итоге добирается до устройства и отключает его
от автономного питания, попутно избегая главного противника — голодной
тыквы.
Теперь напишем более подробный сценарий для трех ключевых моментов игры.
164
СЦЕНАРИЙ
Завязка. Персонаж появляется в точке старта на удалении в 1000 studs от конечного пункта, где располагается синтезирующая машина. Рядом со стартом
будет стенд с кодом для сейфа, где лежит ключ. От старта до финиша проходит
многоуровневая трасса, на которой можно собирать разбросанные по ней яблоки.
Яблоки восстанавливают здоровье. Трасса имеет разрывы, лифты, движущиеся
платформы, лестницы. На трассе есть три точки сохранения.
Конфликт. Игрок должен пройти трассу на время. Если персонаж выпадает
с трассы, то погибает и появляется в точке последнего сохранения. Периодически на пути встречаются различные ГМО-продукты, которые при столкновении
отнимают 5 единиц здоровья, а сами уничтожаются. Также через определенный
интервал времени будет идти дождь из продуктов, и в это время нужно находить
укрытие. Если здоровье персонажа достигнет нуля, то он погибает и появляется
в последней точке сохранения.
По пути к устройству на трассе нужно найти ключ, открывающий электрический
щиток от будки электропитания. Ключ лежит в сейфе, в левом ответвлении от
трассы. Нужно подойти к сейфу и открыть его, введя правильный код.
Развязка. В месте расположения устройства будет электрический щиток, который нужно открыть. Если у персонажа есть ключ, то он открывает щиток
в течение 3 секунд, после чего машина останавливается, а все созданные продукты уничтожаются. Мешать подойти к щитку будет большая перекатывающаяся тыква.
Теперь у нас есть конкретное задание с ключевыми описаниями механики и сюжета в игре.
РАЗРАБОТКА ИГРОВОЙ ЛОКАЦИИ.
СОЗДАНИЕ ПРАВИЛ, БАЗОВОЙ
СТРУКТУРЫ И ЛОГИКИ
По сценарию, площадь локации не должна превышать размера 1000 × 1000 studs —
это стандартный размер локации в шаблонах игр. Создадим локацию, выбрав
Baseplate. Назовем игру GMO. По сценарию, нужно создать противников в виде
овощей, фруктов и других пищевых продуктов. Я взял несколько таких типов
и смоделировал их в Blender. Если ты не умеешь моделировать, то скачай мой
пример игры с GitHub или найди эти игровые модели в Toolbox, куда я их и разместил. На рис. 3.1 представлены эти модели.
165
РИС. 3.1. ИГРОВЫЕ ПРОТИВНИКИ
Для кекса и печенья я добавил текстуры — фотографии реальных продуктов.
Поэтому создал три новых материала, предварительно загрузив фотографии:
текстуру кекса, текстуру печенья, текстуру крема кекса.
Чтобы создать свой материал, открой окно Material Manager и щелкни по знаку +
в нем. Можно выбрать вариант конкретного зарезервированного материала из
Roblox. Полностью свой материал создать не получится: можно взять только
вариант одного из имеющихся. Теперь осталось назвать материал и добавить
изображение в Texture Maps. Для простоты можно добавить во все ячейки одно
и то же изображение с помощью кнопки Import или вставить ID загруженного
изображения (рис. 3.2). Все свои загруженные изображения можно найти во
вкладке Inventory окна Toolbox.
РИС. 3.2. СОЗДАНИЕ СОБСТВЕННОГО МАТЕРИАЛА
Эти материалы будут привязаны только к конкретному проекту. В других проектах придется создавать материалы заново.
166
Теперь сделаем домик, в котором будут появляться игроки после подключения
к игре. Я взял дом из предыдущей книги «Roblox. Играй, программируй и создавай свои миры» (рис. 3.3).
РИС. 3.3. ДОМ ДЛЯ НАЧАЛА ИГРЫ
Принцип создания дома и мебели я подробно описал в предыдущей книге.
Следующий шаг — создание устройства, синтезирующего продукты. Для этих
целей я использую программу Blender. Если ты не умеешь моделировать в редакторе, то можешь создать модель в среде Roblox Studio или скачать готовую
по ссылке в книге. Но я рекомендую проявить фантазию и сделать модель самостоятельно. Ниже представлен пример синтезирующего устройства (рис. 3.4).
РИС. 3.4. СИНТЕЗИРУЮЩЕЕ УСТРОЙСТВО
167
От синей главной части устройства протянут кабель, соединяющийся с электрическим щитком питания (рис. 3.5).
РИС. 3.5. ЩИТОК ЭЛЕКТРОПИТАНИЯ
Следующий шаг — добавление ключа и яблока для здоровья персонажа. Эти модели уже создавались в книге, поэтому я просто перенесу их в этот проект (рис. 3.6).
РИС. 3.6. КЛЮЧ И ЯБЛОКО
168
Теперь создадим сейф, где будет лежать ключ (рис. 3.7).
РИС. 3.7. СЕЙФ С КОДОВЫМ ЗАМКОМ
Мы создали основные игровые объекты и добавили их в проект. Теперь создадим
трассу из платформ. Вручную делать это достаточно сложно. Но можно воспользоваться алгоритмом построения на примере реализации улицы. Следующие
шаги — создание разных вариантов платформ. На рис. 3.8 представлен пример
карты платформера. Этот пример не претендует на полноценную игру, но содержит ряд механик, которые могут быть тебе интересны.
РИС. 3.8. БАЗА КАРТЫ ПЛАТФОРМЕРА
169
На этой схеме есть ключевые элементы платформера:
Start — начало игры;
1 — место сейфа с кодовым замком и ключом в нем;
2 — место с бонусом: тут лежит растворитель органики, который поможет
пройти оставшейся путь и победить босса — тыкву;
Finish — конечная локация игры, где расположены синтезирующее устройство,
босс и кнопка отключения.
Локации 1 и 2 расположены в ответвлениях от основного пути: так мы усложним
игру, сделаем ее более интригующей и поставим игрока перед выбором.
Чтобы прохождение было и не слишком сложным, и не слишком простым, мы
добавим разные препятствия, помеченные буквами:
a — три платформы, вращающиеся параллельно поверхности;
b — два ряда платформ, движущихся параллельно поверхности;
c — несколько платформ с вертикальным перемещением;
d — нестабильные платформы, на которых нужно удержаться;
e — несколько платформ, вращающихся перпендикулярно поверхности;
f — два ряда платформ, движущихся параллельно поверхности, но с противоположным (по сравнению с b) начальным направлением движения.
Это базовая трасса с базовыми элементами препятствий, которые будут дополняться уже другими решениями согласно сценарию. Рассмотрим структуру
реализации описанного элемента всей игровой локации.
Start — это дом, в котором появляется персонаж. Здесь он получит информацию
о своем задании. Внутри дома расположен стартовый SpawnLocation (рис. 3.9).
РИС. 3.9. СТАРТОВАЯ ПОЗИЦИЯ
170
Информация для игроков будет располагаться на экране телевизора. Чтобы не
нарушать общий дизайн, для SpawnLocation укажем параметру Transparency значение 1 (рис. 3.10).
РИС. 3.10. РАБОТА С ЭРГОНОМИКОЙ В ИГРЕ
Когда игрок выходит из дома, то видит такую локацию, как на рис. 3.11.
РИС. 3.11. ВИД ОТ ПЕРВОГО ЛИЦА
171
Чтобы добраться до ключа, игрок должен преодолеть три основных препятствия.
Начнем с препятствия a: три вращающиеся платформы (рис. 3.12).
РИС. 3.12. ВРАЩАЮЩИЕСЯ ПЛАТФОРМЫ
Задача игрока — правильно подобрать темп движения, чтобы пройти эту трассу.
Как реализована эта механика? Очень просто: для каждой платформы добавлены
элементы AngularVelocity и LinearVelocity. Первая отвечает за вращение по определенной оси, а вторая за стабилизацию. Все три платформы названы MAPPlat
(рис. 3.13).
РИС. 3.13. СТРУКТУРА ВРАЩАЮЩЕЙСЯ ПЛАТФОРМЫ
172
Чтобы платформа оставалась на нужной высоте, в элементе LinearVelocity параметру MaxForce задано значение 5 000 000, а параметру VectorVelocity — 0, 0, 0.
Точка приложения указана в центре платформы. В элементе AngularVelocity для
параметра MaxTorque задано значение 5 000 000, а для AngularVelocity — 0, 0.2, 0.
Для центральной платформы настройка будет соответствовать вышеописанному,
а для боковых платформ параметр AngularVelocity будет иметь значение (0, –0.2, 0).
Крайние платформы вращаются против часовой стрелки, а платформа посередине — по часовой. Здесь мы обошлись без скрипта, но ты можешь усовершенствовать логику и настроить изменение скорости вращения через код.
Кроме этого препятствия есть еще одно, помеченное буквой b. Это два ряда
платформ, которые движутся в противоположных направлениях параллельно
поверхности (рис. 3.14).
РИС. 3.14. ДВА РЯДА ДВИЖУЩИХСЯ ПЛАТФОРМ
Дальние от игрока платформы помечены именем platMoveH1, а ближние —
platMoveH2, все вместе они собраны в модель под именем Most. Это сделано для
удобства навигации и работы с каждым объектом игры.
Каждая платформа имеет два элемента: AlignOrientation и LinearVelocity. Для первого
элемента нужно выбрать две опорные точки. Первая — это центр самой платформы, а вторая — поверхность Baseplate. Относительно нее платформа будет
сохранять свою ориентацию в пространстве. Для элемента LinearVelocity нужно
настроить два параметра: MaxForce = 5 000 000 и VectorVelocity = 0, 0, 0.
Чтобы все платформы одного ряда начали синхронно двигаться в одну сторону,
а платформы другого ряда — в противоположную, добавим два скрипта. Один —
для платформ первого ряда с именем platMoveH1, а второй для платформ второго
ряда — platMoveH2 (рис. 3.15).
173
РИС. 3.15. СТРУКТУРА ПЛАТФОРМ МОСТА
Вот скрипт платформ первого ряда:
while true do
for i=0, 5 do
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(-10,0,0)
wait(1)
end
for i=0, 5 do
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(10,0,0)
wait(1)
end
end
Скрипт платформ второго ряда:
while true do
for i=0, 5 do
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(10,0,0)
wait(1)
end
for i=0, 5 do
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(-10,0,0)
wait(1)
end
end
Скрипты различаются направлением скоростей для платформ того или иного
ряда. В моем примере платформы движутся по оси X, но ты можешь сделать и так,
чтобы они двигались по Z. Все зависит от базовой части карты. Платформы удалены друг от друга на равные расстояния как в одном ряду, так и по расстоянию
между двумя рядами. Расстояние между двумя рядами меньше, чтобы можно
было перепрыгивать с одного ряда на другой.
174
После прохождения этой трассы игрок попадает к пункту 1 — месту, где расположены ключи. Поскольку игроков может быть много, то здесь созданы три
сейфа, для каждого из которых нужно подобрать правильный код и взять ключ.
После подбора кода игрок забирает ключ, а сейф закрывается и ждет нового
игрока. Между сейфами и игроками стоит небольшое препятствие — лестница
(рис. 3.16).
РИС. 3.16. ЛЕСТНИЦА ДЛЯ ПОПАДАНИЯ В ЛОКАЦИЮ С СЕЙФАМИ
Чтобы открыть сейф, нужно знать код. Хранить код можно в Workspace в виде
строкового значения. Для этого создадим StringValue, зададим имя Code, а в папке
ServerScriptService создадим скрипт с именем SeifCode. В нем пропишем простую
логику создания кода из четырех цифр, выбранных случайным образом. Этот код
сразу же присвоим переменной Code.
for i = 0, 3 do
game.Workspace.Code.Value..=tostring(math.random(0,9))
end
Этот код должны увидеть игроки. Выведем его на экран телевизора в доме, где
появляется игрок. Для этого найдем в модели House игровой объект Tele и добавим функцию отображения GUI под названием SurfaceGui элементу Frame,
принадлежащему объекту. Теперь на этом элементе можно расположить другие
GUI-инструменты для отображения. Для простоты воспользуемся TextLabel. Теперь
нужно настроить правильное отображение TextLabel на экране.
Убедимся, что текст отображается где-нибудь на экране (спереди, сзади, сверху
или снизу), в зависимости от ориентации Frame. Для решения этих задач ис-
175
пользуются два параметра в свойствах SurfaceGui: AlwaysOnTop и Face. Первый
отображает графические элементы поверх объекта, а второй — на одной из
его сторон. Выбрав нужную сторону, ты увидишь надпись Label на экране телевизора (рис. 3.17).
РИС. 3.17. НАСТРОЙКА ОТОБРАЖЕНИЯ ЭЛЕМЕНТОВ SURFACEGUI
Теперь настроим размер шрифта, цвет и фон. В моем примере фон прозрачный.
Для TextLabel добавим скрипт, где пропишем логику вывода кода после 10 секунд.
Это нужно для того, чтобы точно дождаться формирования кода на сервере и получения переменной Code.
for i = 0, 10 do
game.Workspace.House.Tele.Frame.SurfaceGui.TextLabel.Text = "До появления
кода осталось ".. tostring(10-i)
wait(1)
end
game.Workspace.House.Tele.Frame.SurfaceGui.TextLabel.Text=
game.Workspace.Code.Value
На экране отобразится текст: «До появления кода осталось», а затем игрок увидит
целые числа от 10 до 0. По достижении нуля на экране телевизора отобразится
код, который будет совпадать со значением переменной Code.
176
РИС. 3.18. ОТОБРАЖЕНИЕ КОДА НА ЭКРАНЕ ТЕЛЕВИЗОРА
Теперь об этом коде должны знать сейфы. Можно просто ссылаться на эту переменную, но я сделал для каждого сейфа по одной строковой переменной CodeL,
которая присваивает значение Code. Для этого я добавил им по скрипту, где
прописана логика присвоения, но не сразу, а по истечении 5 секунд с момента
запуска игры.
wait(5)
script.Parent.CodeL.Value = game.Workspace.Code.Value
РИС. 3.19. ДОБАВЛЯЕМ СЕЙФАМ ПЕРЕМЕННУЮ И СКРИПТ ДЛЯ ПРИЕМА КОДА ПЕРЕМЕННОЙ
177
Для взаимодействия с сейфом возьмем инструмент ProximityPrompt, который добавим на ручку дверцы. В свойствах укажем для HoldDuration = 1, чтобы код можно
было активировать в течение 1 секунды, а для MaxActivationDistance = 30, чтобы
уже на расстоянии 30 studs появлялась подсказка с активацией. По умолчанию
оставим клавишу E.
В результате взаимодействия с сейфом должен отобразиться интерфейс для
ввода кода. Этот элемент нужно создать в папке StarterGui. Здесь нужно создать
ScreenGui, а внутри рамку Frame, в которой расположится 12 кнопок TextButton:
цифры от 0 до 9, кнопка Enter и кнопка выхода. Когда игрок будет нажимать на
нужные кнопки интерфейса, их значение должно отображаться там же. Для этого
добавим TextLabel (рис. 3.20).
РИС. 3.20. ИНТЕРФЕЙС ДЛЯ ВВЕДЕНИЯ КОДА
Поскольку графический интерфейс — это локальная конструкция, то нужно добавить строковую переменную, которая для этого случая примет роль локальной.
Зададим ей имя CodeUI. Она будет хранить значение кода с сервера. Поэтому
нужно передать код с сервера на клиент.
Для начала добавим элемент RemoteEvent в папку ReplicatedStorage. Оставим ее
с таким же именем. Передавать значение кода будем после активации дверцы
сейфа. Перейдем в скрипт SeifCode — там добавим процесс взаимодействия через
подсказку и передачу клиенту значения кода из переменной CodeL:
local run = game.ReplicatedStorage.RemoteEvent
local proxy = game:GetService("ProximityPromptService")
local NameObject
for i =0, 3 do
game.Workspace.Code.Value..=tostring(math.random(0,9))
end
-- взаимодействие с сейфом
178
function open(object, player)
print(object.Parent.Parent.Parent)
print(player)
NameObject = object.Parent.Parent.Parent
run:FireClient(player,object.Parent.Parent.Parent.CodeL.Value)
end
proxy.PromptTriggerEnded:Connect(open)
Функции print() нужны для проверки правильности нахождения объекта,
с которым мы взаимодействуем, и для отображения имени игрока, который вза
имодействует с ним. Переменная NameObject нужна для хранения имени сейфа.
Поскольку сейфа три, то нужно знать, с каким именно мы взаимодействуем,
чтобы передать логику анимации открытия двери.
Отправив значение клиенту, то есть на интерфейс игрока, нужно получить это
значение. Для этого в TextLabel добавим локальный скрипт LocalScript. В нем пропишем логику приема данных с сервера и присвоение полученного значения
переменной CodeUI.
local rem = game.ReplicatedStorage.RemoteEvent
rem.OnClientEvent:Connect(function(code)
script.Parent.Parent.Parent.CodeUI.Value = code
end)
Теперь можно вводить код и сверять его с загруженным. Для этого отдельно
создадим LocalScript в ScreenGui (рис. 3.21).
РИС. 3.21. ЛОКАЛЬНЫЙ СКРИПТ ДЛЯ ЛОГИКИ ВВОДА КОДА ПО КНОПКАМ
Перед тем как мы приступим к написанию кода для этого скрипта, объясню
некоторые моменты. Во-первых, интерфейс должен появляться и исчезать. По-
179
скольку он работает на клиенте, то закрыть интерфейс не составит труда, а вот
для открытия потребуется передача команды с сервера на клиент. Во-вторых,
введенный правильный код должен дать команду на открытие дверцы сейфа, а это
означает передачу команды с клиента на сервер. Нужно создать два RemoteEvent
в папке ReplicatedStorage. Назовем их OpenClose и OpenSeif. Первый нужен для отображения интерфейса, а второй для открытия дверцы (рис. 3.22).
РИС. 3.22. УДАЛЕННЫЕ СОБЫТИЯ
Добавим в скрипт SeifCode логику на передачу команды для отображения интерфейса и на прием от клиента данных о верности кода. Вот полный код скрипта:
local run = game.ReplicatedStorage.RemoteEvent
local opclo = game.ReplicatedStorage.OpenClose
local seif = game.ReplicatedStorage.OpenSeif
local proxy = game:GetService("ProximityPromptService")
local NameObject
-- создание кода
for i =0, 3 do
game.Workspace.Code.Value..=tostring(math.random(0,9))
end
-- открытие двери сейфа
seif.OnServerEvent:Connect(function(player, N)
-- если введенный код верен, то принимаем true
if N==true then
print(NameObject)
-- открываем дверцу сейфа, с которым взаимодействовали, и закрываем
local Pivot = game.Workspace[NameObject.name].Door.D:GetPivot()
game.Workspace[NameObject.name].Door.D:PivotTo(Pivot *
CFrame.Angles(0, math.rad(-90), 0))
180
end
end)
wait(3)
game.Workspace[NameObject.name].Door.D:PivotTo(Pivot *
CFrame.Angles(0, math.rad(0), 0))
-- взаимодействие с сейфом
function open(object, player)
print(object.Parent.Parent.Parent)
print(player)
NameObject=object.Parent.Parent.Parent
-- передача кода на локальную переменную
run:FireClient(player,object.Parent.Parent.Parent.CodeL.Value)
-- передача команды на отображение интерфейса
opclo:FireClient(player,true)
end
proxy.PromptTriggerEnded:Connect(open)
Теперь перейдем к логике локального скрипта интерфейса для ввода кода. Для
начала вводим ссылки на удаленные события и прописываем функцию на отображение
интерфейса при взаимодействии с сейфом.
local rem = game.ReplicatedStorage.OpenClose
local seif = game.ReplicatedStorage.OpenSeif
-- отображение интерфейса
rem.OnClientEvent:Connect(function(n)
script.Parent.Frame.Visible=n
end)
Теперь пропишем функции для каждой кнопки 0-9, которые будут отображать
значение в TextLabel.
--функции для кнопок
function zero()
script.Parent.Frame.TextLabel.Text..="0"
end
function one()
script.Parent.Frame.TextLabel.Text..="1"
end
function two()
script.Parent.Frame.TextLabel.Text..="2"
end
function three()
script.Parent.Frame.TextLabel.Text..="3"
end
function four()
script.Parent.Frame.TextLabel.Text..="4"
end
function five()
script.Parent.Frame.TextLabel.Text..="5"
end
function six()
script.Parent.Frame.TextLabel.Text..="6"
181
end
function seven()
script.Parent.Frame.TextLabel.Text..="7"
end
function eight()
script.Parent.Frame.TextLabel.Text..="8"
end
function nine()
script.Parent.Frame.TextLabel.Text..="9"
end
Осталось написать функции для кнопки закрытия интерфейса и для кнопки Enter,
в которой мы проверяем корректность кода.
-- кнопка закрытия окна
function clo()
script.Parent.Frame.Visible=false
end
--кнопка Enter
function ent()
if script.Parent.Frame.TextLabel.Text== script.Parent.CodeUI.Value then
print ("код верен")
script.Parent.Frame.TextLabel.Text="код верен"
-- отправляем на сервер подтверждение ввода кода
seif:FireServer(true)
script.Parent.Key.Value=1
end
else
print ("код неверен")
script.Parent.Frame.TextLabel.Text="код неверен"
end
wait(3)
script.Parent.Frame.TextLabel.Text=''
Заключительным этапом станет вызов этих функций с помощью щелчка левой
клавиши мыши (ЛКМ) по кнопкам.
-- активируем кнопку по нажатии ЛКМ
script.Parent.Frame.B0.MouseButton1Down:Connect(zero)
script.Parent.Frame.B1.MouseButton1Down:Connect(one)
script.Parent.Frame.B2.MouseButton1Down:Connect(two)
script.Parent.Frame.B3.MouseButton1Down:Connect(three)
script.Parent.Frame.B4.MouseButton1Down:Connect(four)
script.Parent.Frame.B5.MouseButton1Down:Connect(five)
script.Parent.Frame.B6.MouseButton1Down:Connect(six)
script.Parent.Frame.B7.MouseButton1Down:Connect(seven)
script.Parent.Frame.B8.MouseButton1Down:Connect(eight)
script.Parent.Frame.B9.MouseButton1Down:Connect(nine)
script.Parent.Frame.Enter.MouseButton1Down:Connect(ent)
script.Parent.Frame.esc.MouseButton1Down:Connect(clo)
182
Осталось проверить работоспособность показанной логики и перейти к следующей части игры. В коде можно заметить некую переменную Key. В нашем случае
это переменная IntValue, которая будет хранить значение о количестве ключей
в интерфейсе (рис. 3.23).
РИС. 3.23. ПЕРЕМЕННАЯ ДЛЯ ХРАНЕНИЯ СОБРАННЫХ КЛЮЧЕЙ
В следующем разделе мы добавим интерфейс для отображения ключа.
После того как игрок добыл ключ, он возвращается тем же путем и идет дальше.
Здесь ему встречается препятствие в виде четырех вертикально движущихся
платформ, которые обозначены буквой c (см. рис. 3.8). Обозначим две платформы
как platMoveV1 и platMoveV2 и расположим их поочередно. Первые будут сначала
двигаться вверх, а потом вниз, вторые — наоборот.
Платформы будут содержать те же инструменты, что и синие платформы
platMoveH1 и platMoveH2: AlignOrientation, LinearVelocity и Script. Для первых двух
элементов настройки те же. При таких настройках платформы должны висеть
в воздухе и не падать. Расположим их на разных высотах так, чтобы игрок перескакивал по каждой, чтобы пройти препятствие (рис. 3.24).
183
РИС. 3.24. ПЛАТФОРМЫ ВЕРТИКАЛЬНОГО ДВИЖЕНИЯ
Для платформ platMoveV1 скрипт будет выглядеть так:
while true do
for i = 0, 5 do
script.Parent.LinearVelocity.VectorVelocity=Vector3.new(0,5,0)
wait(1)
end
for i = 0, 5 do
script.Parent.LinearVelocity.VectorVelocity=Vector3.new(0,-5,0)
wait(1)
end
end
Для платформ platMoveV2 скрипт будет таким:
while true do
for i = 0, 5 do
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(0,-5,0)
wait(1)
end
for i = 0, 5 do
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(0,5,0)
wait(1)
end
end
Следующее препятствие — нестабильные платформы, обозначенные на рис. 2.8
буквой d. Будет 12 платформ из застывшей лавы. Под ними большая плита с именем Lava и соответствующей текстурой. Если персонаж падает в лаву, то погибает
и перерождается в последней сохраненной точке (об этом скажем позже). Платформы сами по себе нестабильны и вращаются вокруг своего центра масс, если
на них наступить. Это может вызвать падение персонажа (рис. 3.25).
184
РИС. 3.25. СТРУКТУРА НЕСТАБИЛЬНЫХ ПЛАТФОРМ
Чтобы добиться такого эффекта, добавим к каждой платформе инструменты:
AlignOrientation, LinearVelocity. Каждая плита имеет материал CrackedLava. Инструмент
AlignOrientation имеет в свойствах MaxTorque значение 31 000, а в Responsiveness —
20. Точки опоры приложим к плите Lava, на которой лежит якорь. Это позволит
платформам вести себя нестабильно. Теперь нужно зафиксировать ее центр
массы. Для этого понадобится LinearVelocity, и в его свойстве MaxForce мы укажем
значение 100 000 (рис. 3.26).
РИС. 3.26. ИНСТРУМЕНТЫ КАЖДОЙ ПЛАТФОРМЫ
185
Из рис. 3.25 видно, что все платформы вместе с лавой я собрал в одну модель Plits,
а каждой плите задал имя Plita и номер от 0 до 11. Это нужно для дальнейших шагов.
Препятствие может оказаться достаточно легким, поэтому добавим щепотку
непредсказуемости. Каждый раз, когда игрок касается плиты, будет вызываться
случайное число из диапазона от 0 до 10. Если выпадет 1, то игровой персонаж
проигрывает и перерождается в последней сохраненной точке. Для этого в модели Plits добавим скрипт с именем RandomTrap, в котором и пропишем эту логику.
-- ссылки на плиты
local p0 = script.Parent.Plita0
local p1 = script.Parent.Plita1
local p2 = script.Parent.Plita2
local p3 = script.Parent.Plita3
local p4 = script.Parent.Plita4
local p5 = script.Parent.Plita5
local p6 = script.Parent.Plita6
local p7 = script.Parent.Plita7
local p8 = script.Parent.Plita8
local p9 = script.Parent.Plita9
local p10 = script.Parent.Plita10
local p11 = script.Parent.Plita11
-- создаем генератор случайных чисел
local r = Random.new(10)
--логическая переменная для задержки
local active = true
-- случайного выбора числа при касании плиты
function trap(player)
if player.Parent:FindFirstChild("Humanoid") then
-- логический ключ для задержки по времени
if active ==true then
print(r:NextInteger(0,10))
--выбор случайного целого числа и сравнение с 1
if r:NextInteger(0,10)==1 then
player.Parent.Humanoid.Health=0
end
--запуск функции задержки по времени
rand()
end
end
end
-- функция задержки по времени
function rand()
active=false
print(active)
wait(2)
active=true
end
-- вызов функций при касании одной из плит
p0.Touched:Connect(trap)
p1.Touched:Connect(trap)
p2.Touched:Connect(trap)
p3.Touched:Connect(trap)
p4.Touched:Connect(trap)
p5.Touched:Connect(trap)
p6.Touched:Connect(trap)
186
p7.Touched:Connect(trap)
p8.Touched:Connect(trap)
p9.Touched:Connect(trap)
p10.Touched:Connect(trap)
p11.Touched:Connect(trap)
Логика такая: при первом касании запускается генератор случайных чисел. Если
выпадает число 1, то персонажу наносится урон, если не 1, то урон не наносится.
Затем вызывается функция rand(), которая отключает логическую переменную
на 2 секунды. В этом случае игрок может сколько угодно касаться плит: случайное
число заново выбираться не будет. По истечении указанного времени процесс
повторяется.
Кто-то сможет пройти препятствие сразу, а кому-то придется повторить попытку. Для усложнения добавим скрипт для лавы, но без случайного выбора числа.
function lava(player)
if player.Parent:FindFirstChild("Humanoid") then
player.Parent.Humanoid.Health=0
end
end
script.Parent.Touched:Connect(lava)
Перейдем к месту, помеченному цифрой 2. Это бонусное место. Здесь игроку
предлагается взять инструмент в виде преобразующей пушки, которая будет расщеплять синтезируемые продукты на атомы. Эта конструкция создается заранее.
Смоделировать ее можно в любой программе.
У пушки есть два элемента, образующие прицел. Из прицела вылетает луч, по
которому летят созданные кубики с именем Sfer + номер кубика (рис. 3.27). Сама
пушка называется TelePort.
РИС. 3.27. АКТИВИРОВАННАЯ С ПОМОЩЬЮ ЛКМ ПУШКА
187
Прицел находится сверху над стволом, и из него вылетают кубики. Для прицела
создается скрипт, где используется объект Ray — луч (прямая, имеющая начало,
но не имеющая конца). Для луча важно указать начало и указать вторую точку,
в которую он должен быть направлен. Луч в игре не может быть бесконечным,
его нужно ограничивать. Ограничителем будет мир локации Roblox и сама вторая точка.
-- создание луча
function RayPart()
-- определяем конечную точку луча
local DirVector = script.Parent.enlRay.Position — script.Parent.Position
-- строим луч
local LuchRay = Ray.new(script.Parent.Position, DirVector)
local part = workspace:FindPartOnRay(LuchRay)
-- если луч определил столкновение с объектом
if part then
-- создание луча не всей длины, а до объекта
local DirVector1 = part.Position — script.Parent.Position
-- цикл построения кубиков, движущихся вдоль луча
for i = 0, math.ceil(DirVector1.Magnitude) do
local sfer = Instance.new("Part",script.Parent)
sfer.CanCollide = false
sfer.Material = Enum.Material.Neon
sfer.Anchored = true
sfer.Size = Vector3.new(1,1,1)
sfer.Position=script.Parent.Position+i*LuchRay.Direction.Unit
sfer.Name = "Sfer"..tostring(i)
wait(0.01)
end
-- удаление созданных кубиков
for i =0, math.ceil(DirVector1.Magnitude) do
local sf = script.Parent["Sfer"..tostring(i)]
sf:Destroy()
wait(0.01)
end
end
end
-- активация функции ЛКМ
script.Parent.Parent.Parent.Activated:Connect(RayPart)
Позже мы пропишем логику взаимодействия с продуктами.
Теперь сделаем так, чтобы каждому игроку была доступна эта пушка. Для простоты сделаем ее дубликат, но уберем прицел со скриптом и добавим подсказку
ProximityPrompt. Подсказка по нажатии клавиши будет появляться, когда игрок
подойдет к дубликату. Как только произойдет нажатие, пушка должна оказаться
в руках игрового персонажа.
Также вытащим модель дубликата из оболочки инструмента (tool) и удалим
Handle (рис. 3.28).
188
РИС. 3.28. ИНСТРУМЕНТ (1) И МОДЕЛЬ (2)
Следующий шаг — отправка инструмента в папку ReplicatedStorage. Нужно сделать
так, чтобы игрок смог положить пушку в свой рюкзак — например, при нажатии подсказки ProximityPromt. Обрабатывать это событие будем в общем скрипте
SeifCode, поскольку именно там и обрабатывали подобные подсказки в прошлый
раз. Согласно документации, обработка таких элементов должна происходить
строго в одном скрипте.
Для этого добавим условие по использованию пушки в функцию open. Чтобы
игрок не смог брать больше одной пушки, добавим условие проверки значения
логической переменной Gun. Если ее значение false, то создаем клон пушки, отправляем в рюкзак игрока и присваиваем значение true. Эта переменная будет
назначаться каждому игроку при его заходе в игру. Код будет чуть ниже, а сейчас
рассмотрим логику функции open.
-- взаимодействие с пушкой и сейфом
function open(object, player)
-- взаимодействуем с пушкой
if (object.Parent.Parent.Name == "Portal_Teleport") then
-- проверка значения переменной игрока
if player:WaitForChild("ValueGun").Gun.Value == false then
local gunClone = game.ReplicatedStorage.Tele_Port:Clone()
gunClone.Parent = player.Backpack
player:WaitForChild("ValueGun").Gun.Value = true
end
end
-- взаимодействуем с сейфом
if(object.Parent.Parent.Name == "Door") then
NameObject=object.Parent.Parent.Parent
-- передача кода на локальную переменную
run:FireClient(player,object.Parent.Parent.Parent.CodeL.Value)
189
end
end
-- передача команды на отображение интерфейса
opclo:FireClient(player,true)
Чтобы код сработал правильно, нужно создать еще один скрипт в папке
ServerScriptService и назвать его AddPlayerValue. В нем мы создадим логическую
переменную Gun для каждого добавленного в игрока.
-- Подключение к папке, где хранятся подключенные игроки
local player = game:GetService("Players")
-- создаем папку для хранения данных каждому подключенному игроку
function DataPlayer(player)
local ValueGun = Instance.new("Folder")
ValueGun.Name = "ValueGun"
ValueGun.Parent = player
local gun = Instance.new("BoolValue", ValueGun)
gun.Name = "Gun"
gun.Parent = ValueGun
end
player.PlayerAdded:Connect(DataPlayer)
Можем протестировать игру от лица сервера для двух игроков. Для этого во
вкладке Test выберем Start. По умолчанию там уже указано два игрока. При запуске откроются три окна — два от лица игроков и одно от лица администратора
игры. На рис. 3.29 представлен результат работы.
РИС. 3.29. ДВА ИГРОКА ВЗАИМОДЕЙСТВУЮТ С ПУШКОЙ
Следующий этап прохождения обозначен буквой e. Этот вид препятствия схож
с препятствием a, но отличается тем, что вращение платформ происходит вокруг
оси, перпендикулярной направлению движения персонажа. Скорость вращения
платформ различна (рис. 3.30).
190
РИС. 3.30. ВРАЩАЮЩИЕСЯ ПЛАТФОРМЫ
Все платформы собраны в одну модель RotPlatform. У каждой платформы есть два
функциональных элемента — AngularVelocity и LinearVelocity. Второй инструмент
нужен для поддержания стабильной устойчивости, поэтому параметр MaxForce
имеет значение 500 000, а скорость по всем осям — ноль. Для первого инструмента настраиваем скорость вращения именно в той плоскости, как задумано
из рис. 3.30. В моем случае это ось x. Скорость вращения варьируется от –0.2 до
0.2. Для параметра MaxTorque установим значение 200 000. У каждой платформы скорость может быть разная, главное, чтобы игроку хватило времени перепрыгнуть с одной на другую (рис. 3.31). Для этого рекомендую несколько раз
протестировать процесс.
После прохождения этого препятствия остается последнее, отмеченное буквой f. Это дубликат горизонтально движущихся платформ, помеченных как b
(рис. 3.32).
После прохождения всех препятствий игроку нужно дойти до электрического
щитка. Если у него есть ключ, то щиток откроется, а игра закончится.
Для тестирования следующего алгоритма расположим сейф с ключом рядом со
щитком, чтобы проверить корректность работы. Если все работает правильно,
то сейф вернем на место.
191
РИС. 3.31. СВОЙСТВА ВРАЩАЮЩИХСЯ ПЛАТФОРМ
РИС. 3.32. ПРЕПЯТСТВИЕ F — ГОРИЗОНТАЛЬНО ДВИЖУЩИЕСЯ ПЛАТФОРМЫ
192
Для начала добавим в скрипт AddPlayerValue логику создания еще одной логической переменной для игрока Key со значением false. Полный код всего скрипта
представлен ниже.
-- подключение к папке, где хранятся подключенные игроки
local player = game:GetService("Players")
-- создаем папку для хранения данных каждому подключенному игроку
function DataPlayer(player)
local ValueGun = Instance.new("Folder")
ValueGun.Name = "ValueGun"
ValueGun.Parent = player
local gun = Instance.new("BoolValue", ValueGun)
gun.Name = "Gun"
gun.Parent = ValueGun
end
-- переменная ключ для игрока
local key = Instance.new("BoolValue", ValueGun)
key.Name = "Key"
key.Value = false
gun.Parent = ValueGun
-- как только игрок подключился, запускаем функцию создания
player.PlayerAdded:Connect(DataPlayer)
Исправим названия элементов электрического щитка, чтобы они не совпадали
с сейфами. Дверцу назовем flapDoor. Этот ящик с дверцей — аналог сейфа, но
только без кнопок. Ручку дверцы назовем knob и разместим на ней подсказку
ProximityPrompt. Дверца — это модель, состоящая из MeshPart и knob. Первый
элемент этой модели — PrimaryPart, где его Pivot смещен в место расположения
петель, как у сейфа (рис. 3.33).
РИС. 3.33. ЭЛЕКТРИЧЕСКИЙ ЩИТОК
193
Осталось прописать логику взаимодействия со щитком: нам нужно отправлять
значение true для переменной Key при правильном подборе пароля. Все это
дописываем в SeifCode. Полный код дополненного скрипта представлен ниже.
local
local
local
local
run = game.ReplicatedStorage.RemoteEvent
opclo = game.ReplicatedStorage.OpenClose
seif = game.ReplicatedStorage.OpenSeif
giv = game.ReplicatedStorage.GiveGun
local proxy = game:GetService("ProximityPromptService")
local NameObject
for i =0, 3 do
game.Workspace.Code.Value..=tostring(math.random(0,9))
end
-- открытие двери сейфа
seif.OnServerEvent:Connect(function(player, N)
-- если введенный код верен, то принимаем true
if N==true then
print(NameObject)
-- открываем дверцу сейфа, с которым взаимодействовали
local Pivot = game.Workspace[NameObject.name].Door.D:GetPivot()
game.Workspace[NameObject.name].Door.D:PivotTo(Pivot *
CFrame.Angles(0, math.rad(-90), 0))
wait(3)
game.Workspace[NameObject.name].Door.D:PivotTo(Pivot *
CFrame.Angles(0, math.rad(0), 0))
-- подтверждаем взятие ключа
player:WaitForChild("ValueGun").Key.Value = true
end
end)
-- взаимодействие с пушкой и сейфом
function open(object, player)
if (object.Parent.Parent.Name == "Portal_Teleport") then
if player:WaitForChild("ValueGun").Gun.Value == false then
local gunClone = game.ReplicatedStorage.Tele_Port:Clone()
gunClone.Parent = player.Backpack
player:WaitForChild("ValueGun").Gun.Value = true
end
end
-- проверяем взаимодействие с дверцей сейфа
if(object.Parent.Parent.Name == "Door") then
NameObject=object.Parent.Parent.Parent
-- передача кода на локальную переменную
run:FireClient(player,object.Parent.Parent.Parent.CodeL.Value)
-- передача команды на отображение интерфейса
opclo:FireClient(player,true)
end
-- проверяем взаимодействие с дверцей щитка
if (object.Parent.Parent.Name == "flapDoor") then
-- проверяем наличие ключа
if (player:WaitForChild("ValueGun").Key.Value==true) then
-- открываем дверцу
local Pivot = game.Workspace.Sintez.Box.flapDoor.
PrimaryPart:GetPivot()
game.Workspace.Sintez.Box.flapDoor.PrimaryPart:PivotTo(Pivot *
CFrame.Angles(0, math.rad(-90), 0))
194
end
end
end
wait(3)
game.Workspace.Sintez.Box.flapDoor.PrimaryPart:PivotTo(Pivot *
CFrame.Angles(0, math.rad(0), 0))
game.Workspace.GameOver.Value = true
print("игра закончена")
proxy.PromptTriggerEnded:Connect(open)
Для полного подтверждения, что игра закончена, будет использоваться глобальная локальная переменная GameOver. Если ее значение станет true, то устройство
сломается и появится надпись Finish.
Основную структуру игры я прописал, теперь добавим дополнительные сложности: появление на пути злобных ГМО-продуктов. Для начала уменьшим их до
приемлемых размеров, сравнимых с размером игрового персонажа (рис. 3.34).
РИС. 3.34. СИНТЕЗИРУЕМЫЕ ПРОДУКТЫ
Добавим каждой основной части модели скрипт по нанесению урона игровому
персонажу, если его коснутся враги. Также добавим логику в функцию касания
для попадания лазера, выходящего из пушки, который будет расщеплять продукты, пока те не пропадут. Разместим скрипт для каждого главного элемента
модели. В моем случае это MeshPart1, являющийся частью PrimaryPart.
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
player.Parent.Humanoid.Health -=2
script.Parent.Parent:Destroy()
end
if player.Name:match("Sfer") then
195
print(player.Name)
script.Parent.Transparency+=0.1
end
end
if script.Parent.Transparency >=0.9 then
script.Parent.Parent:Destroy()
end
script.Parent.Touched:Connect(dum)
Это изначальный скрипт всех продуктов, за исключением тыквы. Она не погибает
при касании с персонажем, а наносит урон. В реальности мы можем не указывать
якорь, и тогда все продукты будут двигаться под действием гравитации. Но для
динамичности можем прописать логику движения, например прыжки и вращение. Если в игре будет слишком много враждебных NPC, их нужно уничтожать
по истечении какого-то времени (например, 20 секунд), чтобы не перегружать
вычислительные мощности сервера и компьютера.
Как видно из рис. 3.34, в игре есть разные синтезируемые пищевые продукты.
Я разделил их на две основные категории: прыгающие и вращающиеся.
К прыгающим относятся грибы, кекс, баклажан и тыква, а к вращающимся —
арбуз и печенье.
Для всех прыгающих продуктов мы добавили два инструмента: AlignOrientation
и LinearVelocity. Оба этих элемента приложены к основному элементу модели
MeshPart1. Для AlignOrientation прикладывают две точки опоры: одна у основания
MeshPart1, а вторая у Baseplate (рис. 3.35).
РИС. 3.35. НАСТРОЙКА ПРЫГАЮЩИХ ПРОДУКТОВ
Рассмотрим скрипт для этой группы игровых объектов.
local run = game:GetService("RunService")
-- нанесение урона
function dum(player)
-- гуманоиду
if player.Parent:FindFirstChild("Humanoid") then
196
end
player.Parent.Humanoid.Health -=2
script.Parent.Parent:Destroy()
-- себе от лазера
if player.Name:match("Sfer") then
print(player.Name)
script.Parent.Transparency+=0.1
end
if script.Parent.Transparency >=0.9 then
script.Parent.Parent:Destroy()
end
end
script.Parent.Touched:Connect(dum)
-- прыгающие движения и время жизни продукта
local timeStart = os.clock()
local t
local timeStart0 = os.clock()
local t0
function drive()
t = os.clock() — timeStart
t0 = os.clock() — timeStart0
if t<0.5 then
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(0,25,-2)
elseif t >0.5 and t<1 then
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(0,-25,-2)
else
timeStart = os.clock()
end
if t0>=20 then
script.Parent.Parent:Destroy()
end
end
run.Stepped:Connect(drive)
Скрипт разбит на две части: функция для отслеживания касаний с объектами
и функция, которая непрерывно запускается каждый кадр. Первая отслеживает
касание с Humanoid и объектом, который содержит в своем имени слово Sfer.
Алгоритм отслеживания касания должен быть тебе понятен, так как был рассмотрен как в этой, так и в предыдущей книге «Roblox. Играй, программируй
и создавай свои миры». Проверка содержания слова Sfer в имени касающегося
объекта происходит с помощью функции match — player.Name:match("Sfer").
Если такой объект касается игрока, то продукт становится прозрачным, пока не
достигнет значения, меньшего 0.9, а после уничтожится.
Вторая часть кода определяет поведение в пространстве. Каждый кадр определяется направлением вектора скорости: вверх на 25 со смещением на 2 по Z в течение полусекунды. Следующие полсекунды скорость направлена вниз (–25) с тем
же смещением по Z. При заданных размерах и материале объект прыгает чуть
выше, чем высота игрового персонажа. Регулируя значение скорости и MaxForce
для LinearVelocity, можно добиться нужного эффекта прыжка и смещения. Для сохранения равновесия в прыжке используется функция AlignOrientation.
197
Для подсчета системного времени используется модуль os, а именно функция
clock. Время, пройденное в игре, находится как разница между текущим временем и временем появления этого объекта.
Можно сделать дублирующие элементы с другими направлениями движения
и обернуть их в алгоритм считывания глобальной переменной Vector3Value.
Однако в нашем примере я не буду это рассматривать: алгоритмов много, и все
они ограничены лишь твоей фантазией и здравым смыслом.
Алгоритм поведения тыквы почти такой же, за исключением касания с персонажем (тыква не исчезает), нанесения урона лазером (оно в 10 раз меньше)
и формой движения — тыква патрулирует место.
local run = game:GetService("RunService")
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
player.Parent.Humanoid.Health -=2
end
if player.Name:match("Sfer") then
print(player.Name)
script.Parent.Transparency+=0.01
end
end
if script.Parent.Transparency >=0.9 then
script.Parent.Parent:Destroy()
end
script.Parent.Touched:Connect(dum)
local timeStart = os.clock()
local t
local timeStart0 = os.clock()
local t0
-- выбор направления смещения
local n
function drive()
t = os.clock() — timeStart
t0 = os.clock() — timeStart0
if t0<=5 then
n=5
elseif t0>5 and t0 <=10 then
n = -5
else
timeStart0 = os.clock()
end
if t<0.5 then
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(0,25,n)
elseif t >0.5 and t<1 then
script.Parent.LinearVelocity.VectorVelocity = Vector3.new(0,-25,n)
else
timeStart = os.clock()
end
end
198
Осталось рассмотреть вращающиеся продукты: печенье и арбуз. Здесь реализация
проще: у обоих стоит только AngularVelocity для назначения вращения (рис. 3.36).
РИС. 3.36. ПАРАМЕТРЫ АРБУЗА
У арбуза и печенья выбраны разные оси вращения. Регулируя значение, выбор
оси и направление вращения, можно выбрать формат движения. Скрипт этих
продуктов немного отличается от предыдущих:
local run = game:GetService("RunService")
local str = "Sfer"
local str0 =''
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
player.Parent.Humanoid.Health -=2
script.Parent.Parent:Destroy()
end
if player.Name:match("Sfer") then
print(player.Name)
script.Parent.Transparency +=0.1
end
end
if script.Parent.Transparency >=0.9 then
script.Parent.Parent:Destroy()
end
script.Parent.Touched:Connect(dum)
local timeStart = os.clock()
local t
function drive()
t = os.clock() — timeStart
199
if t>=20 then
script.Parent.Parent:Destroy()
end
end
run.Stepped:Connect(drive)
После настройки всех элементов мы можем сделать их клоны и расположить
в нужных точках появления. Начнем с арбузов. Создадим клоны арбузов и разместим их в начале трассы — на вершине склона (рис. 3.37).
РИС. 3.37. МЕСТО ПОЯВЛЕНИЯ АРБУЗОВ
Укажем значение скорости вращения для арбузов — (5,0,0). Это значение взято
для корректного движения в разработанной локации. Ты можешь выбрать направление движения вдоль другой оси: главное, протестируй его следующим шагом.
Если движение арбузов отвечает задуманным требованиям, то в их скриптах
можно увеличить значение урона при касании. Затем создадим папку Point1
в разделе ReplicatedStorage и переместим туда три модели арбуза.
Между началом игры и склоном поставим триггер в виде полупрозрачного (или
полностью прозрачного) параллелепипеда (Part) и назовем его TriggerEmeny1.
Создадим для него логическую переменную SpawnValue, а также скрипт, который будет в случайном порядке создавать клоны арбузов в течение 8 секунд до
следующего касания триггера.
local runtime = game:GetService("RunService")
-- касание триггера
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
script.Parent.SpawnValue.Value = true
end
end
script.Parent.CanTouch = false
script.Parent.CanQuery = false
wait(8)
script.Parent.SpawnValue.Value = false
script.Parent.CanTouch = true
script.Parent.CanQuery = true
200
script.Parent.Touched:Connect(dum)
local t =0
local TimeStart =0
local N =0
-- проверка каждого кадра на значение переменной
function timer()
if script.Parent.SpawnValue.Value ==true and N==0 then
TimeStart = os.clock()
N=1
end
if N==1 then
t = os.clock()-TimeStart
print(t)
end
-- каждую секунду создается клон одного из трех арбузов
if t>=1 then
local rand = math.random(1,3)
print(rand)
if rand ==1 then
local meloneClone = game.ReplicatedStorage.Point1.
melon1:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==2 then
local meloneClone = game.ReplicatedStorage.Point1.
melon2:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==3 then
local meloneClone = game.ReplicatedStorage.Point1.
melon3:Clone(workspace)
meloneClone.Parent = workspace
end
N = 0
t = 0
end
end
Если игровой персонаж коснется триггера, то появятся 8 арбузов.
РИС. 3.38. АКТИВАЦИЯ ТРИГГЕРА НА КЛОНИРОВАНИЕ АРБУЗОВ
201
По такому же принципу разместим и остальные продукты.
Сделаем клоны трех грибов. Их задача — появиться недалеко от первых движущихся платформ. Мы разместим их там (рис. 3.39) и установим триггер
TriggerEmeny2 с таким же набором (скрипт и переменная).
РИС. 3.39. КЛОНЫ ГРИБОВ
Убедимся, что клоны грибов движутся на нас: это можно сделать через скрипты,
расположенные в каждом грибе (см. описание выше). После этого разместим их
в папке Point2, которую создадим в разделе ReplicatedStorage.
РИС. 3.40. РАСПОЛОЖЕНИЕ АРБУЗОВ И ГРИБОВ ДЛЯ ДВУХ ТОЧЕК ПОЯВЛЕНИЯ
202
В скрипте для TriggerEmeny2 пропишем следующий код, который будет случайно
создавать каждую секунду сразу три гриба, а не один, как в случае с арбузом.
local runtime = game:GetService("RunService")
-- касание триггера
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
script.Parent.SpawnValue.Value=true
end
end
script.Parent.CanTouch = false
script.Parent.CanQuery = false
wait(10)
script.Parent.SpawnValue.Value=false
script.Parent.CanTouch = true
script.Parent.CanQuery = true
script.Parent.Touched:Connect(dum)
local t =0
local TimeStart =0
local N =0
-- проверка каждого кадра на значение переменной
function timer()
if script.Parent.SpawnValue.Value ==true and N==0 then
TimeStart = os.clock()
N=1
end
if N==1 then
t = os.clock()-TimeStart
print(t)
end
-- каждую секунду создается три клона одного из трех грибов
if t>=2 then
for i =0, 3 do
local rand = math.random(1,3)
print(rand)
if rand ==1 then
local meloneClone = game.ReplicatedStorage.Point2.
mushroom:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==2 then
local meloneClone = game.ReplicatedStorage.Point2.mushroom_
white:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==3 then
local meloneClone = game.ReplicatedStorage.Point2.mushroom_
agaric:Clone(workspace)
meloneClone.Parent = workspace
end
end
N = 0
t = 0
end
end
runtime.Stepped:Connect(timer)
203
РИС. 3.41. ПОЯВЛЕНИЕ ГРИБОВ
Следующая точка появления продуктов — у лестницы к сейфам. Здесь разместим
клоны баклажанов и настроим направление их движения. Также разместим
TriggerEmeny3 со скриптом и переменной, который будет находиться у края вращающихся платформ (рис. 3.42).
РИС. 3.42. БАКЛАЖАНЫ
Дадим имя каждому клону, пронумеруем их и разместим в папке Point3 в разделе ReplicatedStorage. Пропишем логику вызова баклажанов при взаимодействии
с триггером. Каждую секунду мы будем вызывать случайным образом по четыре
баклажана из семи возможных. Чтобы баклажаны не двигались в одну линию,
их скорость будет заранее настроена на разные значения.
local runtime = game:GetService("RunService")
-- касание триггера
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
script.Parent.SpawnValue.Value=true
204
end
end
script.Parent.CanTouch = false
script.Parent.CanQuery = false
wait(10)
script.Parent.SpawnValue.Value=false
script.Parent.CanTouch = true
script.Parent.CanQuery = true
script.Parent.Touched:Connect(dum)
local t =0
local TimeStart =0
local N =0
-- проверка каждого кадра на значение переменной
function timer()
if script.Parent.SpawnValue.Value ==true and N==0 then
TimeStart = os.clock()
N=1
end
if N==1 then
t = os.clock()-TimeStart
print(t)
end
-- каждую секунду создается четыре клона одного из семи баклажанов
if t>=2 then
for i =0, 4 do
local rand = math.random(1,7)
print(rand)
if rand ==1 then
local meloneClone = game.ReplicatedStorage.Point3.
eggplant1:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==2 then
local meloneClone = game.ReplicatedStorage.Point3.
eggplant2:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==3 then
local meloneClone = game.ReplicatedStorage.Point3.
eggplant3:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==4 then
local meloneClone = game.ReplicatedStorage.Point3.
eggplant4:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==5 then
local meloneClone = game.ReplicatedStorage.Point3.
eggplant5:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==6 then
local meloneClone = game.ReplicatedStorage.Point3.
eggplant6:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==7 then
local meloneClone = game.ReplicatedStorage.Point3.
eggplant7:Clone(workspace)
meloneClone.Parent = workspace
end
205
end
N = 0
t = 0
end
end
runtime.Stepped:Connect(timer)
Пример реализации представлен на рис. 3.43.
РИС. 3.43. ПОЯВЛЕНИЕ БАКЛАЖАНОВ
Следующая партия продуктов появится после вертикально движущихся платформ,
перед нестабильными платформами. Мы клонируем кексы и разместим триггер
TriggerEmeny4, у которого также есть скрипт и переменная (рис. 3.44).
РИС. 3.44. КЕКСЫ
206
Настроим скорость движения кексов и их расположение в месте появления. Затем
перенесем в папку Point4 во вкладке ReplicatedStorage. Теперь приступим к редактированию скрипта для триггера, он схож со скриптом для вызова баклажанов.
local runtime = game:GetService("RunService")
-- касание триггера
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
script.Parent.SpawnValue.Value=true
end
end
script.Parent.CanTouch = false
script.Parent.CanQuery = false
wait(10)
script.Parent.SpawnValue.Value=false
script.Parent.CanTouch = true
script.Parent.CanQuery = true
script.Parent.Touched:Connect(dum)
local t =0
local TimeStart =0
local N =0
-- проверка каждого кадра на значение переменной
function timer()
if script.Parent.SpawnValue.Value ==true and N==0 then
TimeStart = os.clock()
N=1
end
if N==1 then
t = os.clock()-TimeStart
print(t)
end
-- каждую секунду создается четыре клона одного из шести кексов
if t>=2 then
for i =0, 3 do
local rand = math.random(1,6)
--print(rand)
if rand ==1 then
local kekClone = game.ReplicatedStorage.Point4.kek1:Clone(workspace)
kekClone.Parent = workspace
elseif rand ==2 then
local kekClone = game.ReplicatedStorage.Point4.kek2:Clone(workspace)
kekClone.Parent = workspace
elseif rand ==3 then
local kekClone = game.ReplicatedStorage.Point4.kek3:Clone(workspace)
kekClone.Parent = workspace
elseif rand ==4 then
local kekClone = game.ReplicatedStorage.Point4.kek4:Clone(workspace)
kekClone.Parent = workspace
elseif rand ==5 then
local kekClone = game.ReplicatedStorage.Point4.kek5:Clone(workspace)
kekClone.Parent = workspace
elseif rand ==6 then
local kekClone = game.ReplicatedStorage.Point4.kek6:Clone(workspace)
207
kekClone.Parent = workspace
end
end
N = 0
t = 0
end
end
runtime.Stepped:Connect(timer)
РИС. 3.45. ПОЯВЛЕНИЕ КЕКСОВ
После прохождения нестабильных платформ снова будет склон. Здесь можно
реализовать вызов арбузов, как в первоначальном варианте. Создадим три клона
арбузов и поставим триггер TriggerEmeny5 (рис. 3.46).
РИС. 3.46. ПОЯВЛЕНИЕ АРБУЗОВ В СЛЕДУЮЩЕЙ ТОЧКЕ
208
Теперь настроим вращение арбузов по склону вниз. Скорее всего, нужно будет
указать скорость по другой оси. В этом примере я укажу вращение со скоростью 2 вокруг оси z. После этого разместим эти объекты в папке Point5 во вкладке
ReplicatedStorage.
Скрипт вызова клонов будет таким же, как и в первом варианте, за исключением
выбора папки.
local runtime = game:GetService("RunService")
-- касание триггера
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
script.Parent.SpawnValue.Value=true
end
end
script.Parent.CanTouch = false
script.Parent.CanQuery = false
wait(8)
script.Parent.SpawnValue.Value=false
script.Parent.CanTouch = true
script.Parent.CanQuery = true
script.Parent.Touched:Connect(dum)
local t =0
local TimeStart =0
local N =0
-- проверка каждого кадра на значение переменной
function timer()
if script.Parent.SpawnValue.Value ==true and N==0 then
TimeStart = os.clock()
N=1
end
if N==1 then
t = os.clock()-TimeStart
print(t)
end
-- каждую секунду создается клон одного из трех арбузов
if t>=1 then
local rand = math.random(1,3)
print(rand)
if rand ==1 then
local meloneClone = game.ReplicatedStorage.Point5.
melon1:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==2 then
local meloneClone = game.ReplicatedStorage.Point5.
melon2:Clone(workspace)
meloneClone.Parent = workspace
elseif rand ==3 then
local meloneClone = game.ReplicatedStorage.Point5.
melon3:Clone(workspace)
meloneClone.Parent = workspace
end
209
N = 0
t = 0
end
end
runtime.Stepped:Connect(timer)
Обрати внимание: всем клонам перед размещением в папки прописывается
свое имя (название продукта + цифра). По этим названиям они и вызываются
в скриптах.
РИС. 3.47. ПОЯВЛЕНИЕ АРБУЗОВ НА ВТОРОМ СКЛОНЕ
Взобравшись на склон, персонаж увидит большое пространство — на него нужно
зайти и затем либо взять пушку, либо идти дальше. В любом случае мы касаемся
созданного триггера TriggerEmeny6 и генерируем копию печенья Cookie (рис. 3.48).
РИС. 3.48. ТРИГГЕР ПОСЛЕ ПРЕОДОЛЕНИЯ СКЛОНА И ПЕЧЕНЬЕ
210
Для печенья напишем еще один скрипт, который будет перемещать его в случайном порядке по всей области этого поля. Для этого укажем, в каких значениях изменяются координаты x и z на этой платформе. Просто передвигай печенье к краям поля и выписывай значения. В моем случае это: x ∈ [–384, –212], z ∈ [–407,
–192]. Используя эти данные, напишем логику скрипта с печеньем (рис. 3.49).
РИС. 3.49. СОЗДАНИЕ ВТОРОГО СКРИПТА ДЛЯ ИЗМЕНЕНИЯ ПОЛОЖЕНИЯ ПЕЧЕНЬЯ
Код скрипта:
local Pivot = script.Parent.PrimaryPart:GetPivot()
local x = math.random(-384, -212)
local z = math.random(-407, -192)
script.Parent.PrimaryPart:PivotTo(CFrame.new(x, 230, z))
Значение y = 230, поскольку сама платформа находится на высоте 182. На печенье действует гравитация, поэтому оно будет падать. Переместим печенье
в папку Point6 во вкладке ReplicatedStorage.
Теперь перейдем к написанию скрипта для триггера. Он простой: это создание
20 штук печенья каждые 2 секунды в течение 10 секунд.
local runtime = game:GetService("RunService")
-- касание триггера
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
script.Parent.SpawnValue.Value=true
end
end
script.Parent.CanTouch = false
script.Parent.CanQuery = false
wait(10)
script.Parent.SpawnValue.Value=false
script.Parent.CanTouch = true
script.Parent.CanQuery = true
script.Parent.Touched:Connect(dum)
211
local t =0
local TimeStart =0
local N =0
-- проверка каждого кадра на значение переменной
function timer()
if script.Parent.SpawnValue.Value ==true and N==0 then
TimeStart = os.clock()
N=1
end
if N==1 then
t = os.clock()-TimeStart
print(t)
end
-- каждые две секунды создаются 20 клонов печенья
if t>=2 then
for i =0, 20 do
local CookieClone = game.ReplicatedStorage.Point6.
Cookie:Clone(workspace)
CookieClone.Parent = workspace
end
end
N = 0
t = 0
end
runtime.Stepped:Connect(timer)
Результат работы скрипта представлен на рис. 3.50.
РИС. 3.50. ПАДАЮЩЕЕ ПЕЧЕНЬЕ
Такой же дубликат триггера можно поставить там, где расположены лестницы,
ведущие к пушке. Ее можно будет сразу опробовать на печенье.
После прохождения этого поля и платформ персонажа ожидает еще одно подобное поле. На нем можно реализовать тот же алгоритм с печеньем. Для этого
создадим клон печенья и напишем границы его появления на этой платформе.
У меня получилось так: x ∈ [–389, –200], z ∈ [–960, –689].
212
После разместим триггер в папке Point7 во вкладке ReplicatedStorage. Дадим ему
имя TriggerEmeny7 и используем тот же алгоритм для скрипта, что и в предыдущем,
заменив адрес на Point7.
Теперь персонажу осталось преодолеть синие платформы и встретиться с тыквами. Здесь мы можем разместить последний триггер, при касании с которым
появится множество тыкв в разных точках платформы.
Возьмем алгоритм от печенья, так как у него схожая механика. Теперь добавим
к копии тыквы скрипт для ее вызова в игре. Тыква должна появиться на ограниченной области, исходя из промежутков значений по X и Z. В моем случае это
будет: x ∈ [306, 778], z ∈ [–960, –387]. Значение Y = 180.
Ты можешь установить свои значения в зависимости от того, где будут появляться
клоны тыквы. Размер области регулируется заданными интервалами, указанными
выше, для x и z.
Согласно этому диапазону, тыква появляется от триггера до синтезирующей
установки.
Вот второй скрипт тыквы:
local Pivot = script.Parent.PrimaryPart:GetPivot()
local x = math.random(306, 778)
local z = math.random(-960, -387)
script.Parent.PrimaryPart:PivotTo(CFrame.new(x, 180, z))
Теперь разместим тыкву в папке Point8 и перейдем к скрипту вызова тыкв при касании
триггера.
local runtime = game:GetService("RunService")
-- касание триггера
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
script.Parent.SpawnValue.Value=true
end
end
script.Parent.CanTouch = false
script.Parent.CanQuery = false
wait(10)
script.Parent.SpawnValue.Value=false
script.Parent.CanTouch = true
script.Parent.CanQuery = true
script.Parent.Touched:Connect(dum)
local t =0
local TimeStart =0
local N =0
-- проверка каждого кадра на значение переменной
function timer()
if script.Parent.SpawnValue.Value ==true and N==0 then
TimeStart = os.clock()
N=1
end
213
if N==1 then
t = os.clock()-TimeStart
print(t)
end
-- каждые две секунды создаются 20 клонов тыкв
if t>=2 then
for i =0, 20 do
local CookieClone = game.ReplicatedStorage.Point8.
pumpkin1:Clone(workspace)
CookieClone.Parent = workspace
end
N = 0
t = 0
end
end
runtime.Stepped:Connect(timer)
Результат работы скрипта представлен на рис. 3.51.
РИС. 3.51. ПОЯВЛЕНИЕ ТЫКВ — ФИНАЛЬНЫХ БОССОВ В ИГРЕ
Итак, работа над структурой игры почти закончена. Осталось добавить места
быстрого сохранения, и можно переходить к оформлению интерфейса.
Наложим на Baseplate изображение песка или лавы и добавим скрипт уничтожения персонажа при соприкосновении с тыквой.
function dum(player)
if player.Parent:FindFirstChild("Humanoid") then
player.Parent.Humanoid.Health =0
end
end
script.Parent.Touched:Connect(dum)
Также создадим облака. Для этого во вкладке Terrain добавим функцию Clouds
(рис. 3.52).
214
РИС. 3.52. НАСТРОЙКА ПОВЕРХНОСТИ АТМОСФЕРЫ
Для восстановления здоровья можем добавить яблоки со скриптом:
function apple_money(player)
if player and player.Parent:FindFirstChild("Humanoid") then
player.Parent.Humanoid.Health +=2
game.Workspace.appleSound.Value = true
script.Parent:Destroy()
end
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
Обрати внимание: в скрипте есть ссылка на логическую переменную appleSound.
Ее задача — включать звук сбора. Затем мы продублируем яблоки и раскидаем
их по локации (рис. 3.53).
РИС. 3.53. ЯБЛОКИ-БОНУСЫ
Последним штрихом будет добавление CheckPoint. Их количество будет равно количеству точек появления продуктов, то есть восьми. Возьмем готовый вариант
из ToolBox, вбив в модели слово checkpoint (рис. 3.54).
215
РИС. 3.54. ОРИГИНАЛ SPAWNLOCATION И CHECKPOINT ИЗ TOOLBOX
В выбранной модели уже есть скрипт для хранения данных о положении для
персонажа.
Осталось сделать восемь таких дубликатов и разместить их по точкам. На этом
работа над структурой игрового мира закончена, и мы переходим к разработке
интерфейса игры.
РАЗРАБОТКА ИНТЕРФЕЙСА
Приступим к разработке интерфейса. Мы провели подготовительную работу,
когда создавали сейф. Разместим весь графический интерфейс в папке StarterGui.
Чтобы он отображался, все UI-элементы нужно размещать в объекте ScreenGui.
Там размещена рамка с имитацией кодового замка сейфа Frame. Создадим еще
одну рамку, которая будет содержать изображение ключа, и текстовое поле, отображающее наличие ключа у игрока.
РИС. 3.55. ИНТЕРФЕЙС ДЛЯ ОТОБРАЖЕНИЯ КЛЮЧА
В localScript проверяется верность кода для сейфа. Там есть строка, начисляющая
единицу переменной Key, если код верен. Сюда же добавим строку на отображение значения в интерфейсе.
216
Ниже приведен фрагмент кода с уже вставленной строкой:
function ent()
if script.Parent.Frame.TextLabel.Text== script.Parent.CodeUI.Value then
print ("код верен")
script.Parent.Frame.TextLabel.Text="код верен"
seif:FireServer(true)
-- начисляем значение ключа
script.Parent.Key.Value=1
-- отображаем в интерфейсе
script.Parent.FrameKey.TextLabel.Text=1
end
else
print ("код неверен")
script.Parent.Frame.TextLabel.Text="код неверен"
end
wait(3)
script.Parent.Frame.TextLabel.Text=''
Теперь добавим интерфейс, который будет отображать пройденное время. Для
определения (отсчета) времени используем модуль os. Сначала создадим TextLabel
и назовем TextTime. Укажем предварительный текст для TextLabel и установим
масштабируемость текста (рис. 3.56).
РИС. 3.56. ИНТЕРФЕЙС ДЛЯ ВРЕМЕНИ
Напишем простую логику включения счетчика времени при появлении игрового персонажа в игре. Создадим скрипт во вкладке ServerScriptService с именем
AddPlayer и добавим удаленное событие eventTime во вкладку ReplicatedStorage.
Пропишем в скрипте AddPlayer следующий код:
local
local
local
local
Run = game:GetService("RunService")
eventTime = game.ReplicatedStorage.eventTime
Player = game:GetService("Players")
ActiveTime = false
local startTime = 0
local t = 0
local Player0
217
-- активируем таймер для добавленного игрока
function Start(play)
ActiveTime = true
startTime = os.clock()
Player0 = play
end
Player.PlayerAdded:Connect(Start)
-- если таймер активирован, то отправляем время на UI игрока
function Timer()
if ActiveTime == true then
t = os.clock() — startTime
eventTime:FireClient(Player0, math.ceil(t))
end
end
Run.Stepped:Connect(Timer)
Функция os.clock() определяет текущее системное время, а startTime — время,
когда появился персонаж. Для округления времени до целых секунд используем
math.ceil().
Как видно, время отправляется клиенту, то есть игроку. Теперь создадим локальный скрипт, например принадлежащий TextTime, и пропишем код на прием
значений по этому событию.
local eventTime = game.ReplicatedStorage.eventTime
eventTime.OnClientEvent:Connect(function(t)
script.Parent.Text="Время = ".. tostring(t)
end)
Результат должен быть таким, как на рис. 3.57.
РИС. 3.57. ВРЕМЯ РАБОТЫ ТАЙМЕРА
Простой интерфейс для игры готов!
218
ЗВУКОВОЕ СОПРОВОЖДЕНИЕ
Теперь добавим звук сбора предметов, звук столкновения с врагами и фоновую
музыку. Звуки можно найти в Toolbox или загрузить свои через AssetManager либо
через личный кабинет с сайта Roblox (см. первые главы).
Выберем фоновую музыку в Toolbox: будем искать по названию funny и выберем
Funny Sunny (рис. 3.58).
РИС. 3.58. ДОБАВЛЕНИЕ МУЗЫКАЛЬНОГО ФОНА ИГРЫ
Скопируем ID трека и вставим его для объекта Sound , который создадим
в Workspace. Здесь же создадим еще два объекта Sound и назовем их Bonus и Damage.
Добавим им звуковые эффекты при нанесении урона и получения здоровья. Будем
искать в Toolbox по ключевым именам (рис. 3.59–3.60).
219
РИС. 3.59. ЗВУК УРОНА
РИС. 3.60. ЗВУК БОНУСА
В Workspace разместим две логические переменные, DamageBool и appleSound.
Последняя переменная уже добавлялась при настройке яблока. Обе переменные
имеют начальное значение false. В Workspace создадим скрипт SoundScript, где
пропишем логику включения звука.
220
local sound0 = game.Workspace.Bonus
local sound1 = game.Workspace.Damag
Run = game:GetService("RunService")
function ActivSound()
if game.Workspace.appleSound.Value==true then
game.Workspace.appleSound.Value=false
sound1:Stop()
sound0:Play()
end
if game.Workspace.DamageBool.Value==true then
game.Workspace.DamageBool.Value=false
sound0:Stop()
sound1:Play()
end
end
Run.Stepped:Connect(ActivSound)
У яблока уже есть строка, где установлено значение переменной appleSound =
true. Теперь добавим строку и присвоим значение true переменной DamageBool.
Для этого в каждом клоне продукта во вкладке ReplicatedStorage добавим в их
общий скрипт (см. скрипт у MeshPart1) строку в функцию, обрабатывающую
касание:
game.Workspace.DamageBool.Value = true
Теперь у нас есть звуковые эффекты, интерфейс, сюжет и логика игры. Приступаем к тестированию и публикации.
ТЕСТИРОВАНИЕ, ОТЛАДКА
И ПУБЛИКАЦИЯ ИГРЫ
Протестируем игру, выявим баги и постараемся их устранить. Все ошибки мы
можем отследить в окне Output.
Тестировать игру рекомендуется с момента ее наполнения и как можно чаще.
Все то, что было описано в книге выше, тестировалось много десятков раз. Разработчикам игр приходится часами играть в собственную игру, чтобы настроить
ее так, как задумывалось изначально.
Для тестирования есть специальная вкладка Test, имеющая несколько режимов.
Нас интересуют три (рис. 3.61):
запуск игры без игроков;
запуск игры с игроком;
запуск игры от лица нескольких игроков и сервера.
221
РИС. 3.61. РЕЖИМЫ ТЕСТИРОВАНИЯ ИГРЫ
В режиме Play мы играем от лица игрока и тестируем поведение игры на локальном компьютере. Мы проверяем, как работают все элементы игры.
В режиме Run мы тестируем игру без игровых персонажей. Мы проверяем, как
работает логика игровых объектов, которым не нужно взаимодействовать с персонажем.
В режиме Start мы тестируем поведение игры и нескольких игроков. Мы проверяем корректность работы игры для всех игроков и стабильность работы сервера.
Первые два режима часто используются при отладке, поэтому сейчас мы запустим
третий режим для двух игроков. В зависимости от мощности компьютера запуск
такой симуляции может занять разное время. Рекомендую предварительно сохранить проект.
На рис. 3.62 представлен вариант тестирования игры для двух игроков. Слева —
вид с сервера, а справа — два окна двух игроков.
222
РИС. 3.62. ТЕСТИРОВАНИЕ МУЛЬТИПЛЕЕРА ИГРЫ
Если поведение игры ожидаемое, то можно приступать к ее публикации. Для
этого во вкладке File нужно щелкнуть по Publish to Roblox (рис. 3.63).
РИС. 3.63. ПУБЛИКАЦИЯ ИГРЫ В ROBLOX
223
Появится окно, в котором нужно указать название игры и ее краткое описание.
Нажав Save, можно перейти во вкладку Game Setting и продолжить настраивать
игру: добавить изображения (рис. 3.64), сделать игру публичной (рис. 3.65).
РИС. 3.64. ДОБАВЛЯЕМ СКРИНШОТЫ ИГРЫ
В качестве изображений можно загрузить как скриншоты игры, так и художественные композиции. Главное, чтобы изображения не нарушали ничьи авторские права.
Созданная игра может иметь три вида публичности:
доступна только для друзей в Roblox;
доступна для всех пользователей Roblox;
никому не доступна, кроме разработчика.
224
РИС. 3.65. УКАЗЫВАЕМ, КТО МОЖЕТ ИГРАТЬ В ИГРУ
Эту игру ты можешь найти в Roblox и сыграть в нее: https://www.roblox.com/
games/11808330799/GMO-adventures.
ЗАДАНИЕ
Добавь текстовую подсказку для игрока, которая объясняет, что ему нужно
делать в этой игре.
4
ПЕСОЧНИЦА:
ТВОРЧЕСКАЯ
ИГРА
Песочница — жанр, где у игроков либо нет конечной цели,
либо она размыта и ее достижение не прекращает игру. Достигая цели, игрок все равно может заниматься творчеством
или другими действиями, которые реализованы в игре.
Главная особенность этих игр — возможность создавать (конструировать) и добывать что-то. Она может иметь только
творческий режим или дополнительные режимы, например
режим выживания, где нужно достичь какой-то цели: выбраться с острова, построить город, спасти тайных жителей
от главного злодея.
Ты можешь вспомнить множество подобных игр, подходящих
под это описание.
226
ТВОРЧЕСТВО РАДИ ТВОРЧЕСТВА:
АНАЛИЗ ЖАНРА
В отличие от других игр песочницы позволяют пользователям взаимодействовать
с разнообразными игровыми объектами и создавать из них другие объекты, тем
самым изменяя частично или полностью элементы локации или саму локацию:
архитектуру, ландшафт, разнообразие инструментов и устройств и т. д.
Логика основных элементов прописана в игре, но, опираясь на эту логику, можно
создавать то, чего заранее не было заложено. Например, у игрока есть инструменты и логика для создания колес, моторов и кузовов или платформ. При этом
не запрещено прикреплять мотор к разным частям устройства, добавлять два,
три или восемь колес и соединять трактор с кораблем. В этом и прелесть песочниц — в творческом взаимодействии с подручным материалом. В разных играх
механики песочницы могут быть реализованы в разной степени.
В этой книге я не смогу описать полноценное создание песочницы, но некоторые
механики мы рассмотрим. Итак, начнем с истории.
История. Игровой персонаж попадает на большой остров, где есть огромный
замок, огороженный забором. В замке есть портал, с помощью которого игрок
может вернуться домой. Герой выясняет, что открыть ворота и пройти в замок
может лишь тот, кто поставит на определенный постамент 100 000 кг добытого
на острове золота.
Теперь игроку нужно придумать способы добычи этого металла. У него есть
брошюра с инструкциями по созданию устройств и инструментов по добыче
золота. Пока игрок добывает золото, а процесс этот небыстрый, то параллельно
обустраивает локацию: ферму, жилье и создает современные инструменты для
приготовления пищи, шитья одежды и т. д.
Когда игрок решается войти в замок, то выясняет, что это жилье Кощея. Он забирает все золото игрока и его вещи, но в обмен открывает портал домой или
предлагает поменяться местами. Если игрок соглашается на первое условие, то
возвращается домой. Если выбирает второе, то сам становится Кощеем, а тот
становится смертным человеком и возвращается к людям. Во втором случае
герой остается жить на острове.
Составим дизайнерский документ игры.
ЛОКАЦИЯ
Игра будет состоять из одной локации не больше 2000 × 2000 studs. Бˆольшую
часть будет занимать остров, окруженный океаном. На острове есть разно
образный биом1: пустыня, смешанный лес, долина и гора. У отвесного края располагается замок Кощея.
1
Земля с различной почвой, ландшафтом, водой, лавой, горными породами и т. д.
227
ИНСТРУМЕНТАРИЙ
На острове можно найти палки и камни, из которых можно сделать топор, кирку,
мотыгу, молоток.
Топор: рубка деревьев и добыча древесины.
Кирка: добыча руды.
Мотыга: вспахивание земли для посадки растений.
Молоток: ремонт и создание вещей.
Из срубленной древесины строим жилище, устройства для добычи золота и другие конструкции. Руда может содержать золото, базальт, железо или уголь. Со
срубленных деревьев (яблоня, хлебное дерево) падают плоды, которые можно
съесть или посадить для культивирования. На острове растет дикий сахарный
тростник. При выборе материала молоток может соединять детали в конструкции.
ЗАМОК
Большой замок стоит на краю острова так, чтобы одна сторона смотрела на
океан с отвесного обрыва. Замок огорожен каменным забором, и есть ворота,
рядом с которыми находится небольшой постамент для того, чтобы положить
золото, а на стене забора установлена надпись о том, как попасть в замок.
Основную информацию мы подготовили. Это пример, и ты сам можешь придумать и описать свою игру.
РАЗРАБОТКА ЛОКАЦИИ И ПРАВИЛ
Приступаем к составлению правил для игры и разработки локации. Правила
также можно было описать в дизайнерском документе, но я специально вынес
их в этот раздел, чтобы уделить им внимание.
ПРАВИЛА
Так как действие будет разворачиваться на острове, с которого герой не может
просто так уйти, то первым правилом игры будет ограничение пространства.
Как только игрок отплывает на 20 studs от берега, то появляется надпись, что он
утонул. Затем игрок оказывается в стартовой точке.
Замок будет огражден неприступным забором, который нельзя перепрыгнуть.
Каждый игровой час у игрока понижается уровень сытости, поэтому он должен
питаться плодами деревьев, рыбой и растениями.
Нельзя подниматься выше 200 studs и углубляться в землю до 200 studs.
228
С помощью кирки можно добывать золото. Его количество может быть разным
в зависимости от местности:
песок: 1 грамм золота;
чернозем: 1 грамм золота;
базальт: 500 граммов золота;
вода: 10 граммов золота (только для драги).
В песке, черноземе и базальте золото добывается после одного клика левой кнопкой мыши. В воде золото добавляется автоматически каждую минуту.
В зависимости от местности можно найти дополнительную руду. Если это горная
порода, то там будет железо, уголь, базальт. Если песок — то уголь. Если чернозем — то небольшие кусочки железа.
Со срубленного дерева падает два вида фруктов: яблоки и плоды хлебного дерева.
Яблоко восстанавливает сытость на 5, плод хлебного дерева на 20. Сытость определяется по шкале от 0 до 100. Если соединить плод хлебного дерева с сахарным
тростником, то получим пирог, который утолит голод на 30 единиц.
Базовые конструкции:
плод хлебного дерева + тростник = пирог;
палка + камень = молоток;
палка + два камня = кирка;
две палки = мотыга;
две палки и два камня = топор.
Все созданные инструменты добавляются в рюкзак персонажа, инструменты
можно держать в руке.
Ресурсы и то, что из них получают:
дерево — бревна;
палки — деревянные цилиндры;
камни — небольшие Part;
руда — кубические Part разного размера.
Дополнительные конструкции:
палка + уголь = факел;
десять камней + уголь = печь;
печь + железная руда = железо;
три железа = дверь.
229
Мы обозначили базовые правила. В процессе ты сможешь усложнить игру, добавив новые правила и конструкции.
ЛОКАЦИЯ
Приступим к созданию локации. Для начала сгенерируем остров с помощью
Terrarien Editor. Этот инструмент я упоминал в главе 1 «Знакомство с Roblox Studio»
и подробно рассматривал его в предыдущей книге «Roblox. Играй, программируй
и создавай свои миры».
Сгенерируем ландшафт, предварительно указав размер и материал, из которого
будет состоять местность (рис. 4.1).
РИС. 4.1. ГЕНЕРАЦИЯ МИРА
В моем примере получилась вот такая базовая конструкция (рис. 4.2). С помощью
этого же инструментария сгладим края острова, используя вкладку Edit. Вокруг
острова добавим воду, имитирующую океан.
230
РИС. 4.2. СГЕНЕРИРОВАННЫЙ ОСТРОВ
Далее используем инструменты редактирования и скорректируем сгенерированный остров: сгладим края, добавим русла рек, создадим породы (песок, базальт,
вода, гравий и т. д.; рис. 4.3).
РИС. 4.3. ДОБАВЛЯЕМ ОКЕАН И КОРРЕКТИРУЕМ ОСТРОВ
Теперь приступим к созданию замка. Выберем край острова и выровняем участок
под замок.
231
Для замка я возьму модель, которую показывал в разделе «Импорт 3D-моделей,
анимации и текстур» в главе 2. Разместим модель и отмасштабируем ее, затем
добавим стены и ворота. За стеной сделаем цилиндрический триггер, который
поведет игрока к финальной сцене, если тот наберет 100 000 килограммов золота
и откроет ворота (рис. 4.4 и 4.5).
РИС. 4.4. РАЗМЕЩЕНИЕ ЗАМКА
РИС. 4.5. ПЕРЕД ВХОДОМ В ЗАМОК
232
На стене у ворот разместим табличку с именем Info — это простой Part с добавлением элемента SurfaceGui. Для этого элемента мы можем добавить UI-объект,
который будет отображаться на табличке. В нашем случае это будет TextLabel.
По умолчанию UI-элементы отображаются на стороне Front детали Part. В свойствах SurfaceGui есть параметр Face, который и позволяет менять отображение
UI-элементов по сторонам модели. Если у тебя не отобразился TextLabel, значит,
неправильно выбрана сторона в SurfaceGui (рис. 4.6).
РИС. 4.6. НАСТРОЙКА SURFACEGUI
Теперь разберемся, как игрок может разместить золото и проверить его перед
входом в замок. На рис. 4.6 представлен пример. Игроку достаточно встать на
постамент, и нужный скрипт проверит количество золота. Используем здесь
скрипт на событие касания, а хранить данные будем в DataStore.
Для этого воспользуемся алгоритмом создания папки на сервере для хранения
данных игрока (см. главу 2, раздел «Игровой опыт»). Опубликуем игру и включим
возможность работать с данными на сервере. Затем создадим скрипт DataPlayer во
вкладке ServerScriptService, где пропишем логику сохранения данных и их чтения
на сервере и с сервера.
-- подключение к папке, где хранятся подключенные игроки
local player = game:GetService("Players")
-- подключение к сервису хранения данных и создания таблицы Progress
local progressStore = game:GetService("DataStoreService"):GetDataStore("Progress")
233
-- создаем папку для хранения данных каждого подключенного игрока
function DataPlayer(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local money = Instance.new("NumberValue", leaderstats)
money.Name = "Gold"
money.Parent = leaderstats
end
-- создание ключа
local keyMoney = "MassGold"..player.UserId
print (keyMoney)
-- создание значения
local dataMoney
-- подключаемся для чтения данных значения
local success, err = pcall(function()
dataMoney = progressStore:GetAsync(keyMoney)
end)
if success then
money.Value = dataMoney
else
print("Error connect ...")
end
-- как только игрок подключился, запускаем функцию создания
хранения значений добытого золота
player.PlayerAdded:Connect(DataPlayer)
function SavePlayer(player)
-- создание ключа
local keyMoney = "MassGold"..player.UserId
print (keyMoney)
-- сохранение значения
local dataMoney = player.leaderstats.Gold.Value
-- подключаемся для записи данных значения
local success, err = pcall(function()
progressStore:SetAsync(keyMoney, dataMoney)
end)
if success then
print("Data Store")
else
print("Error connect ...")
warn(err)
end
end
-- при выходе игрока данные сохраняются
player.PlayerRemoving:Connect(SavePlayer)
переменной для
234
На рис. 4.7 показан результат тестирования сохранения и чтения данных.
РИС. 4.7. СОХРАНЕНИЕ ДАННЫХ НА СЕРВЕРЕ И ИХ ЧТЕНИЕ
После входа в игру переключимся на сторону сервера, во вкладке Players найдем
своего персонажа и папку leaderstats, в которой лежит переменная Gold. Изменим
ее значение на 100 и перейдем в режим игрока. Через 6 секунд выйдем из игры.
Снова зайдем в игру и посмотрим, сохранились ли значения.
За режим переключения с сервера на клиента отвечает кнопка Current Client
(Server).
РИС. 4.8. ПЕРЕКЛЮЧЕНИЕ РЕЖИМОВ «СЕРВЕР — КЛИЕНТ»
Приступим к созданию игровых объектов, которые отвечают за получение ресурсов: дерево, золото, железная руда, базальт, уголь (рис. 4.9).
235
РИС. 4.9. ОСНОВНЫЕ РЕСУРСЫ В ИГРЕ
Дерево дает три ресурса: яблоко (1), плод хлебного дерева (2), доску (3)
(рис. 4.10).
РИС. 4.10. РЕСУРСЫ ДЕРЕВА
236
По изначальному плану в игре должны быть ветки, камни и сахарный тростник
(рис. 4.11).
РИС. 4.11. МОДЕЛИ ТРОСТНИКА, КАМНЯ И ВЕТКИ
Согласно дизайн-документу, в игре должны расти деревья, а игрок всегда может
найти палки и камни. Есть два пути: либо клонировать подобранный или срубленный объект в случайном месте карты острова, либо определить места для
их возрождения (клонирования) заранее.
Первый способ хорош тем, что делает игру более непредсказуемой, но требует
от разработчика написания сложной логики определения высот местности, на
которой будет появляться клон. Иначе клоны могут оказаться под или над землей.
Второй способ облегчает работу разработчику, но делает предсказуемым появление объекта. Здесь мы рассмотрим второй способ, который намного проще
в реализации.
Создадим part с именем SpawnTree и установим ему толщину 0.5 studs, сопоставимую по площади со стволом дерева. Добавим к нему скрипт с кодом, который
вызывает клон модели дерева и привязывает клон к SpawnTree.
local cloneTree = game.ReplicatedStorage.Tree:Clone()
cloneTree.PrimaryPart:PivotTo(script.Parent.CFrame)
cloneTree.Parent = script.Parent
Клоны будут создаваться, если дерево расположено в ReplicatedStorage. Но перед
тем как отправлять модель дерева в эту вкладку, выберем PrimaryPart (в моем
237
случае это ствол) и опустим точку приложения Pivot к основанию ствола. Относительно этой точки и будет позиционироваться вся модель (рис. 4.12).
РИС. 4.12. НАСТРОЙКА ТОЧКИ ОПОРЫ МОДЕЛИ ДЕРЕВА
Настроив модель, отправляем ее во вкладку ReplicatedStorage. Нужно убедиться,
что все части модели приварены или соединены другими инструментами, но не
заякорены (Ancored == false). Если на ее части стоит якорь, кроме PrimaryPart, то
этот элемент не будет смещаться в указанную точку без прямого указания на нее.
Расположим объекты SpawnTree произвольно по всей локации, дополнительно
увеличим им прозрачность и поставим якорь. Так как это деревья, то выберем
для них чернозем (траву) и грязь (рис. 4.13).
РИС. 4.13. РАСПОЛОЖЕНИЕ SPAWNTREE
Разместим объекты, выделим их все и объединим в модель с именем Forest. Теперь нужно протестировать локацию. Если все сделано правильно, то результат
должен быть таким, как показано на рис. 4.14.
238
РИС. 4.14. ЛЕС ИЗ КЛОНИРОВАННЫХ ДЕРЕВЬЕВ
Аналогично можно расположить ветки, камни и сахарный тростник. Для интереса тростник можно сделать редким игровым объектом, чтобы у игрока была
мотивация искать его и выращивать.
Пример реализации тростника, камня и ветки представлен на рис. 4.15. Эти
игровые объекты также размещаются для клонирования в ReplicatedStorage.
РИС. 4.15. ИГРОВЫЕ ОБЪЕКТЫ ДЛЯ КЛОНИРОВАНИЯ ТРОСТНИКА, ВЕТКИ И КАМНЯ
Приступим к разработке инструментов для добычи ресурсов.
239
РАЗРАБОТКА ИГРОВЫХ ИНСТРУМЕНТОВ
ДЛЯ ТВОРЧЕСТВА
Нам нужно смоделировать или взять из Toolbox следующие инструменты: кирку,
топор, молоток и мотыгу. Я использую 3D-редактор, чтобы создать эти модели по
частям: сначала рукоятку, потому насадку. Ты можешь скачать готовые модели
из Toolbox, но имей в виду, что они могут содержать лишние скрипты.
Все описанные в этой книге модели и локации можно скачать по ссылке на мой
GitHub. В приложении в конце книги указаны ID всех опубликованных моделей.
Эти модели в приложении ты можешь использовать в любых своих играх без
ограничений.
РИС. 4.16. МОДЕЛИРОВАНИЕ ИНСТРУМЕНТОВ
Чтобы модель в Roblox состояла из разных деталей, ее нужно экспортировать по
частям (см. главу 2, раздел «Импорт 3D-моделей, анимации и текстур»).
Назовем модели так: кирка — Pickax, молоток — Hammer, мотыга — Motyg,
топор — Axe.
240
Если все сделано правильно, то в редакторе Roblox ты увидишь модели
(рис. 4.17).
РИС. 4.17. ЧЕТЫРЕ ИНСТРУМЕНТА
Рукоятки всех моделей — это PrimaryPart, остальные части приварены к рукоятке.
Сделаем из этих моделей инструменты: вставим каждую модель в элемент Tool
и дадим им одноименные названия. К основанию каждой рукоятки приварим part
с именем Handle. Оптимизируем масштаб модели под персонажа и протестируем
на правильность держания в руке (рис. 4.18 и 4.19).
РИС. 4.18. СОЗДАНИЕ ИНСТРУМЕНТОВ ИЗ МОДЕЛЕЙ
241
РИС. 4.19. УДЕРЖАНИЕ ИНСТРУМЕНТОВ В ИСХОДНОМ ПОЛОЖЕНИИ
Как видно из рис. 4.19, у всех частей моделей есть названия. Это особенно важно
для тех частей, которые будут контактировать с игровыми объектами локации —
землей и деревьями. В моем случае это лезвие топора Sword, насадка кирки Kirk,
насадка молотка Molot и насадка мотыги Tyapka. Эти имена будут обрабатываться
для определения тех или иных действий.
Следующий шаг — добавить анимацию при нажатии левой кнопки мыши. Это
можно сделать в локальном скрипте в папке StarterPlayerScripts.
Для этого в папке ReplicatedStorage добавим элемент Animation, для которого подберем анимацию. Я возьму тот вариант, который использовал для анимации
меча в разделе «Игровой опыт». Логику скрипта возьму из раздела «Движение,
анимация, симуляция (физика и эффекты)», где обрабатывается нажатие левой
кнопки мыши.
---- определяем локального игрока
local player = game:GetService("Players").LocalPlayer
wait(2)
local player0 = player.Character:WaitForChild("Humanoid")
242
---- находим анимацию
local anim = game.ReplicatedStorage.Animation
----подключаем анимацию к игроку
local anim0 = player0:LoadAnimation(anim)
anim0.Looped = false
local ContextActionService = game:GetService("ContextActionService")
-- функция по работе с событиями пользователя
local function handleAction(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
if actionName == "Sword" then
print("Удар")
anim0:Play()
end
end
end
ContextActionService:BindAction("Sword", handleAction, true,
Enum.UserInputType.MouseButton1)
Сейчас Humanoid:LoadAnimation считается поддерживаемым, но уже устаревшим
способом вызова анимации. В современном варианте анимация подключается
через animator, который должен находиться в Humanoid.
Поэтому строку local anim0 = player0:LoadAnimation(anim) можно заменить так:
-- находим у гуманоида элемент animator
local animator = player0:WaitForChild("Animator")
-- подключаем анимацию к animator
local anim0 = animator:LoadAnimation(anim)
Инструменты должны как-то воздействовать на игровые объекты.
Начнем с дерева. Плоды яблони, хлебного дерева и доску также переместим
в ReplicatedStorage.
Перейдем к модели дерева и создадим скрипт. Пропишем логику, при которой
дерево становится прозрачным при каждом ударе топора (то есть лезвия Sword).
Как только значение прозрачности достигнет 0.9, дерево станет полностью
прозрачным и свойства на обработку касания отключатся. Затем активируется
таймер на 10 секунд, после которого дерево появляется. За включение таймера
отвечает логическая переменная Active, которую можно добавить в модель дерева.
Как только дерево исчезает, появляются три предмета: яблоко, плод хлебного
дерева и доска. Скрипт представлен ниже. Модель дерева по умолчанию состоит
из двух частей: ствола Tree3 и кроны Tree2.
Run = game:GetService("RunService")
-- логика рубки дерева
function felling(part)
print(part)
if part.Name == "Sword" then
script.Parent.Tree2.Transparency+=0.1
script.Parent.Tree3.Transparency+=0.1
243
end
end
-- полное исчезновение дерева
if script.Parent.Tree3.Transparency>0.9 then
script.Parent.Tree2.CanTouch = false
script.Parent.Tree3.CanTouch = false
script.Parent.Tree2.Transparency=1
script.Parent.Tree3.Transparency=1
script.Parent.Active.Value = true
end
script.Parent.Tree3.Touched:Connect(felling)
local TimeStart = 0
local t =0
local n =0
-- каждый кадр проверяем значение логической переменной Active
function updateTree ()
if script.Parent.Active.Value == true then
-- клон яблока
local applyClone = game.ReplicatedStorage.apple:Clone()
applyClone.PrimaryPart:PivotTo(script.Parent.Tree3.CFrame*CFrame.
new(0,0,-5))
applyClone.Parent = workspace
-- клон плода хлебного дерева
local BreadFruitClone = game.ReplicatedStorage.Breadfruit:Clone()
BreadFruitClone.Position = script.Parent.Tree3.Position + Vector3.
new(0,0,5)
BreadFruitClone.Parent = workspace
-- клон доски
local WoodPlankClone = game.ReplicatedStorage.WoodPlank:Clone()
WoodPlankClone.Position = script.Parent.Tree3.Position+ Vector3.
new(0,0,10)
WoodPlankClone.Parent = workspace
-- начало отсчета времени до появления дерева
TimeStart=os.clock()
script.Parent.Active.Value = false
n =1
end
-- включение таймера
if n==1 then
t = os.clock()-TimeStart
end
-- отключение таймера
if t >=10 then
n =0
script.Parent.Tree2.Transparency = 0
script.Parent.Tree3.Transparency = 0
script.Parent.Tree2.CanTouch = true
script.Parent.Tree3.CanTouch = true
t = 0
end
end
Run.Stepped:Connect(updateTree)
244
Клоны яблока, плода хлебного дерева и доска появляются со сдвигом на 5 или
10 studs от центра расположения дерева. Также у изначальных объектов, на
основе которых делается клон, отключен якорь, поэтому они свободно падают
(рис. 4.20).
РИС. 4.20. ДОБЫВАЕМ РЕСУРСЫ ИЗ ДЕРЕВА
Теперь нужно собрать эти ресурсы и запомнить. Для этого допишем логику
сохранения данных в DataStore. В скрипт DataPlayer добавим код, где создадим
папку CountData. В этой папке создадим десять переменных, хранящих данные
о ресурсах: яблоко, плод хлебного дерева, доска, ветка, камень, сахарный тростник, базальт, железная руда, уголь и железо (сталь).
Полный код DataPlayer будет выглядеть так:
-- подключение к папке, где хранятся подключенные игроки
local player = game:GetService("Players")
-- подключение к сервису хранения данных и создания таблицы Progress
local progressStore = game:GetService("DataStoreService"):GetDataStore("Progress")
-- создаем папку для хранения данных каждого подключенного игрока
function DataPlayer(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local money = Instance.new("NumberValue", leaderstats)
money.Name = "Gold"
money.Parent = leaderstats
-- создание ключа
local keyMoney = "MassGold"..player.UserId
print (keyMoney)
-- создание значения
local dataMoney
245
-- подключаемся для чтения данных значения
local success, err = pcall(function()
dataMoney = progressStore:GetAsync(keyMoney)
end)
if success then
money.Value = dataMoney
else
print("Error connect ...")
end
-- папка с сохраненными данными собранных предметов
local CountData = Instance.new("Folder")
CountData.Name = "CountData"
CountData.Parent = player
-- количество яблок
local Countapple = Instance.new("IntValue", CountData)
Countapple.Name = "CountApple"
Countapple.Parent = CountData
-- количество плодов хлебного дерева
local CountBread = Instance.new("IntValue", CountData)
CountBread.Name = "CountBread"
CountBread.Parent = CountData
-- количество досок
local CountWood = Instance.new("IntValue", CountData)
CountWood.Name = "CountWood"
CountWood.Parent = CountData
-- количество базальта
local CountBaselt = Instance.new("IntValue", CountData)
CountBaselt.Name = "CountBaselt"
CountBaselt.Parent = CountData
-- количество железной руды
local CountIronOre = Instance.new("IntValue", CountData)
CountIronOre.Name = "CountIronOre"
CountIronOre.Parent = CountData
-- количество железа
local CountIron = Instance.new("IntValue", CountData)
CountIron.Name = "CountIron"
CountIron.Parent = CountData
-- количество угля
local CountCoal = Instance.new("IntValue", CountData)
CountCoal.Name = "CountCoal"
CountCoal.Parent = CountData
-- количество тростника
local CountCane = Instance.new("IntValue", CountData)
CountCane.Name = "CountCane"
CountCane.Parent = CountData
-- количество камней
local CountStone = Instance.new("IntValue", CountData)
CountStone.Name = "CountStone"
CountStone.Parent = CountData
246
-- количество веток
local CountBranch = Instance.new("IntValue", CountData)
CountBranch.Name = "CountBranch"
CountBranch.Parent = CountData
-- создание ключа
local keyapple = "Apple"..player.UserId
local keybread = "Bread"..player.UserId
local keywood = "Wood"..player.UserId
local keybaselt = "Baselt"..player.UserId
local keyironore = "IronOre"..player.UserId
local keyiron = "Iron"..player.UserId
local keycoal = "Coal"..player.UserId
local keycane = "Cane"..player.UserId
local keystone = "Stone"..player.UserId
local keybranch = "Branch"..player.UserId
-- создание значения
local dataApple
local dataBread
local dataWood
local dataBaselt
local dataIronOre
local dataIron
local dataCoal
local dataCane
local dataStone
local dataBranch
-- подключаемся для чтения данных значения
local success, err = pcall(function()
dataApple = progressStore:GetAsync(keyapple)
dataBread = progressStore:GetAsync(keybread)
dataWood = progressStore:GetAsync(keywood)
dataBaselt = progressStore:GetAsync(keybaselt)
dataIronOre = progressStore:GetAsync(keyironore)
dataIron = progressStore:GetAsync(keyiron)
dataCoal = progressStore:GetAsync(keycoal)
dataCane = progressStore:GetAsync(keycane)
dataStone = progressStore:GetAsync(keystone)
dataBranch = progressStore:GetAsync(keybranch)
end)
if success then
Countapple.Value = dataApple
CountBread.Value = dataBread
CountWood.Value = dataWood
CountBaselt.Value = dataBaselt
CountIronOre.Value = dataIronOre
CountIron.Value = dataIron
CountCoal.Value = dataCoal
CountCane.Value = dataCane
CountStone.Value = dataStone
CountBranch.Value = dataBranch
else
print("Error connect ...")
end
end
247
-- как только игрок подключился, запускаем функцию создания
player.PlayerAdded:Connect(DataPlayer)
function SavePlayer(player)
-- создание ключа
local keyMoney = "MassGold"..player.UserId
print (keyMoney)
-- сохранение значения
local dataMoney = player.leaderstats.Gold.Value
-- подключаемся для записи данных значения
local success, err = pcall(function()
progressStore:SetAsync(keyMoney, dataMoney)
end)
if success then
print("Data Store")
else
print("Error connect ...")
warn(err)
end
-- создание ключа
local keyapple = "Apple"..player.UserId
local keybread = "Bread"..player.UserId
local keywood = "Wood"..player.UserId
local keybaselt = "Baselt"..player.UserId
local keyironore = "IronOre"..player.UserId
local keyiron = "Iron"..player.UserId
local keycoal = "Coal"..player.UserId
local keycane = "Cane"..player.UserId
local keystone = "Stone"..player.UserId
local keybranch = "Branch"..player.UserId
-- сохранение значения
local dataApple =player.CountData.CountApple.Value
local dataBread =player.CountData.CountBread.Value
local dataWood = player.CountData.CountWood.Value
local dataBaselt = player.CountData.CountBaselt.Value
local dataIronOre = player.CountData.CountIronOre.Value
local dataIron = player.CountData.CountIron.Value
local dataCoal = player.CountData.CountCoal.Value
local dataCane = player.CountData.CountCane.Value
local dataStone = player.CountData.CountStone.Value
local dataBranch = player.CountData.CountBranch.Value
local success, err = pcall(function()
progressStore:SetAsync(keyapple, dataApple)
progressStore:SetAsync(keybread, dataBread)
progressStore:SetAsync(keywood, dataWood)
progressStore:SetAsync(keybaselt, dataBaselt)
progressStore:SetAsync(keyironore, dataIronOre)
progressStore:SetAsync(keyiron, dataIron)
progressStore:SetAsync(keycoal, dataCoal)
progressStore:SetAsync(keycane, dataCane)
progressStore:SetAsync(keystone, dataStone)
progressStore:SetAsync(keybranch, dataBranch)
end)
248
if success then
print("Data Store")
else
print("Error connect ...")
warn(err)
end
end
-- при выходе данные игрока сохраняются
player.PlayerRemoving:Connect(SavePlayer)
Если тебе трудно воспринимать такой код, то создавай по одной переменной и тестируй ее. Если все сделано правильно, при запуске игры во вкладке Players отобразится имя персонажа и две папки: leaderstats и CountData. В последней и должны
находиться все десять переменных со значением по умолчанию 0 (рис. 4.21).
РИС. 4.21. СОЗДАНИЕ ЦЕЛОЧИСЛЕННЫХ ПЕРЕМЕННЫХ ДЛЯ ХРАНЕНИЯ ДАННЫХ О РЕСУРСАХ
Теперь пропишем логику сбора и начисления значений переменным. Для модели
и для целого объекта обработка события касания будет разной. В моем примере
модели — это яблоко и сахарный тростник. Все остальные ресурсы — цельные
объекты.
Скрипт для яблока:
local Players = game:GetService("Players")
function apple_money(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной игрока CountApple
player0:WaitForChild("CountData").CountApple.Value += 1
249
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent:Destroy()
end
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
Скрипт для сахарного тростника:
local Players = game:GetService("Players")
function apple_money(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной CountCane
player0:WaitForChild("CountData").CountCane.Value += 5
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent.Parent:Destroy()
script.Parent:Destroy()
end
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
Название функции я не менял, но эти два скрипта лежат в разных моделях.
Скрипт для плода хлебного дерева:
local Players = game:GetService("Players")
function BreadfruitAdd(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной CountBread
player0:WaitForChild("CountData").CountBread.Value += 1
if player and player.Parent:FindFirstChild("Humanoid") then
end
script.Parent:Destroy()
end
script.Parent.Touched:Connect(BreadfruitAdd)
Скрипт для доски:
local Players = game:GetService("Players")
function PlankAdd(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной CountWood
player0:WaitForChild("CountData").CountWood.Value+=1
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent:Destroy()
end
250
end
script.Parent.Touched:Connect(PlankAdd)
Скрипт для камня:
Run = game:GetService("RunService")
local Players = game:GetService("Players")
function StoneAdd(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной CountStone
player0:WaitForChild("CountData").CountStone.Value+=1
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent.Active.Value = true
script.Parent.Transparency = 1
script.Parent.CanTouch = false
script.Parent.CanQuery = false
--script.Parent:Destroy()
end
end
script.Parent.Touched:Connect(StoneAdd)
local TimeStart = 0
local t =0
local n =0
-- каждый кадр проверяем значение логической переменной Active
function updateStone()
if script.Parent.Active.Value == true then
-- начало отсчета времени до появления камня
TimeStart=os.clock()
script.Parent.Active.Value=false
n =1
end
-- включение таймера
if n==1 then
t = os.clock()-TimeStart
end
-- отключение таймера
if t >=10 then
n =0
script.Parent.Transparency=0
script.Parent.CanTouch = true
script.Parent.CanQuery = true
t = 0
end
end
Run.Stepped:Connect(updateStone)
Скрипт для ветки:
Run = game:GetService("RunService")
local Players = game:GetService("Players")
function BranchAdd(player)
251
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной CountBranch
player0:WaitForChild("CountData").CountBranch.Value += 1
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent.Active.Value = true
script.Parent.Transparency = 1
script.Parent.CanTouch = false
script.Parent.CanQuery = false
--script.Parent:Destroy()
end
end
script.Parent.Touched:Connect(BranchAdd)
local TimeStart = 0
local t =0
local n =0
-- каждый кадр проверяем значение логической переменной Active
function updateBranch()
if script.Parent.Active.Value == true then
-- начало отсчета времени до появления ветки
TimeStart=os.clock()
script.Parent.Active.Value=false
n =1
end
-- включение таймера
if n==1 then
t = os.clock()-TimeStart
end
-- отключение таймера
if t >=10 then
n =0
script.Parent.Transparency=0
script.Parent.CanTouch = true
script.Parent.CanQuery = true
t = 0
end
end
Run.Stepped:Connect(updateBranch)
Для оставшихся ресурсов код будет похожим, за исключением золотой руды.
В этом случае нужно присваивать значение в папку leaderstats.
Пример скрипта для золотой руды:
local Players = game:GetService("Players")
function GoldAdd(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной Gold
player0:WaitForChild("leaderstats").Gold.Value += 1
if player and player.Parent:FindFirstChild("Humanoid") then
252
end
script.Parent:Destroy()
end
script.Parent.Touched:Connect(GoldAdd)
Перейдем к скрипту сахарного тростника, который растет в дикой местности.
Эта модель состоит из десяти частей, поэтому прописываем это в скрипте:
local Players = game:GetService("Players")
Run = game:GetService("RunService")
function apple_money(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной CountCane
player0:WaitForChild("CountData").CountCane.Value+=1
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent.Active.Value = true
script.Parent.PrimaryPart.Transparency = 1
script.Parent.PrimaryPart.CanTouch = false
script.Parent.PrimaryPart.CanQuery = false
script.Parent.MeshPart1.Transparency = 1
script.Parent.MeshPart1.CanTouch = false
script.Parent.MeshPart1.CanQuery = false
script.Parent.MeshPart2.Transparency = 1
script.Parent.MeshPart2.CanTouch = false
script.Parent.MeshPart2.CanQuery = false
script.Parent.MeshPart3.Transparency = 1
script.Parent.MeshPart3.CanTouch = false
script.Parent.MeshPart3.CanQuery = false
script.Parent.MeshPart4.Transparency = 1
script.Parent.MeshPart4.CanTouch = false
script.Parent.MeshPart4.CanQuery = false
script.Parent.MeshPart5.Transparency = 1
script.Parent.MeshPart5.CanTouch = false
script.Parent.MeshPart5.CanQuery = false
script.Parent.MeshPart6.Transparency = 1
script.Parent.MeshPart6.CanTouch = false
script.Parent.MeshPart6.CanQuery = false
script.Parent.MeshPart7.Transparency = 1
script.Parent.MeshPart7.CanTouch = false
script.Parent.MeshPart7.CanQuery = false
script.Parent.MeshPart8.Transparency = 1
script.Parent.MeshPart8.CanTouch = false
script.Parent.MeshPart8.CanQuery = false
253
script.Parent.Part.Transparency = 1
script.Parent.Part.CanTouch = false
script.Parent.Part.CanQuery = false
--script.Parent.Parent:Destroy()
--script.Parent:Destroy()
end
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
local TimeStart = 0
local t =0
local n =0
-- каждый кадр проверяем значение логической переменной Active
function updateStone()
if script.Parent.Active.Value == true then
-- начало отсчета времени до появления тростника
TimeStart=os.clock()
script.Parent.Active.Value = false
n =1
end
-- включение таймера
if n==1 then
t = os.clock()-TimeStart
end
-- отключение таймера
if t >=1000 then
n =0
script.Parent.PrimaryPart.Transparency = 0
script.Parent.PrimaryPart.CanTouch = true
script.Parent.PrimaryPart.CanQuery = true
script.Parent.MeshPart1.Transparency = 0
script.Parent.MeshPart1.CanTouch = true
script.Parent.MeshPart1.CanQuery = true
script.Parent.MeshPart2.Transparency = 0
script.Parent.MeshPart2.CanTouch = true
script.Parent.MeshPart2.CanQuery = true
script.Parent.MeshPart3.Transparency = 0
script.Parent.MeshPart3.CanTouch = true
script.Parent.MeshPart3.CanQuery = true
script.Parent.MeshPart4.Transparency = 0
script.Parent.MeshPart4.CanTouch = true
script.Parent.MeshPart4.CanQuery = true
script.Parent.MeshPart5.Transparency = 0
script.Parent.MeshPart5.CanTouch = true
script.Parent.MeshPart5.CanQuery = true
script.Parent.MeshPart6.Transparency = 0
script.Parent.MeshPart6.CanTouch = true
script.Parent.MeshPart6.CanQuery = true
254
script.Parent.MeshPart7.Transparency = 0
script.Parent.MeshPart7.CanTouch = true
script.Parent.MeshPart7.CanQuery = true
script.Parent.MeshPart8.Transparency = 0
script.Parent.MeshPart8.CanTouch = true
script.Parent.MeshPart8.CanQuery = true
end
end
script.Parent.Part.Transparency = 0
script.Parent.Part.CanTouch = true
script.Parent.Part.CanQuery = true
t = 0
Run.Stepped:Connect(updateStone)
Поскольку сахарный тростник — редкое растение, разместим его на локации
в малых количествах. Он будет возрождаться после сбора только через 1000 секунд.
Обрати внимание: у многих моделей и объектов добавлена глобальная переменная Active, которая связана с таймером.
Для клонирования ветки, камня и сахарного тростника создадим три part с именами BranchClone, StoneClone, CaneClone. Каждый из них будет содержать скрипт
предварительного вызова клонов перечисленных игровых ресурсов:
Скрипт для part ветки:
local cloneBranch = game.ReplicatedStorage.Branch:Clone()
cloneBranch.Position= script.Parent.Position
cloneBranch.Parent = script.Parent
Скрипт для part камня:
local cloneStone = game.ReplicatedStorage.Stone:Clone()
cloneStone.Position = script.Parent.Position
cloneStone.Parent = script.Parent
Скрипт для part сахарного тростника:
local cloneCane = game.ReplicatedStorage.Cane0:Clone()
cloneCane.PrimaryPart:PivotTo(script.Parent.CFrame)
cloneCane.Parent = script.Parent
Обрати внимание, что создается клон дикого тростника, который должен появляться через 1000 секунд. Его имя в ReplicatedStorage описано так: Cane0.
Теперь осталось проверить, как работают эти ресурсы. После сбора они должны
появиться вновь. Камень и ветка возникают через 10 секунд, тростник — через
1000 (рис. 4.22).
255
РИС. 4.22. МЕСТО ПОЯВЛЕНИЯ РЕСУРСОВ
Для всех ресурсов, кроме выращенного тростника, будет одно начисление, равное 1. У тростника будет 5. В дальнейшем ты сможешь настроить начисление
значений на свое усмотрение.
Вернемся к инструментам. Работу топора мы уже настроили, теперь перейдем
к написанию логики мотыги. Мотыга должна рыхлить землю, при этом нельзя
рыхлить камень. Вспахать поверхность мотыгой можно, если это трава, песок,
песчаник, снег, земля. Все эти элементы перечислены в классе Material класса Enum
(https://create.roblox.com/docs/reference/engine/enums/Material).
Как только мотыга коснется поверхности земли, материал поменяется на мокрую
землю — грязь. Для этой реализации используем функционал класса Terrain,
https://create.roblox.com/docs/reference/engine/classes/Terrain. В документации описана
функция ReplaceMaterial, задача которой — менять один материал на другой. Эта
функция содержит минимум четыре параметра: Region3, номер размерности,
материал, который нужно заменить, и материал, который будут установлен. Размерность кисти должна быть не меньше 4 из-за особенности строения воксельной
структуры Terrain. Параметр Region3 — это параллелепипед области, которая будет
изменяться. В нашем случае нужна небольшая поверхность.
Создадим скрипт для насадки мотыги, которая называется Tyapka. Укажем
в скрипте таблицу материалов, с которыми можно взаимодействовать и которые можно заменять, и укажем материал Mud — он будет заменять площадь
под мотыгой.
256
-- определяем, с каким материалом будем работать
local materialToReplace = {Enum.Material.Ice, Enum.Material.Sand, Enum.Material.
Sandstone, Enum.Material.Grass, Enum.Material.Ground, Enum.Material.Glacier}
-- определяем материал, которым будем заменять площадь
local replacementMaterial = Enum.Material.Mud
function TeriaMater(part)
if part.Name =="Terrain" then
-- заменяем один материал на другой
for i in ipairs(materialToReplace) do
-- функция, которую лучше записать в одну строку
workspace.Terrain:ReplaceMaterial(
Region3.new(script.Parent.Position — Vector3.new(10,2,10), script.Parent.Position
+ Vector3.new(10,2,10)), 4, materialToReplace[i], replacementMaterial)
end
end
end
script.Parent.Touched:Connect(TeriaMater)
Если игрок начнет бить мотыгой по поверхности земли с соответствующим материалом, то земля поменяется на грязь.
Доработаем эту логику. Если щелкнуть правой кнопкой мыши, то на сырой земле
должен вырасти сахарный тростник. В другом месте он не будет расти.
Вот что нужно сделать для этого:
Создать удаленное событие RayLuch и разместить в ReplicatedStorage. Дописать
обработку правой кнопки мыши в локальном скрипте для пользователя и передать сообщение через удаленное событие на скрипт в Tyapka.
В скрипте, расположенным в Tyapka, прописать прием удаленного события
с вызовом функции. В ней будет создаваться луч, который обрабатывает касание. Луч нужно направить вниз к земле, чтобы он определил материал. Если
материал — Mud, то в точке касания создается клон тростника.
Для реализации первого шага нужно создать удаленное событие RayLuch
в ReplicatedStorage, а затем перейти во вкладку StarterPlayerScripts, где расположен наш локальный скрипт, обрабатывающий нажатие левой кнопки мыши.
Допишем несколько строк для обработки событий правой кнопки мыши.
Вот полный код локального скрипта:
local rayEvent = game.ReplicatedStorage.RayLuch
---- определяем локального игрока
local player = game:GetService("Players").LocalPlayer
wait(2)
local player0 = player.Character:WaitForChild("Humanoid")
---- находим анимацию
local anim = game.ReplicatedStorage.Animation
----подключаем анимацию к игроку
257
local anim0 = player0:LoadAnimation(anim)
anim0.Looped = false
local ContextActionService = game:GetService("ContextActionService")
-- функция по работе с событиями от пользователя
local function handleAction(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
if actionName == "Sword" then
print("Удар")
anim0:Play()
end
if actionName == "Luch" then
rayEvent:FireServer(true)
end
end
end
-- левая кнопка мыши
ContextActionService:BindAction("Sword", handleAction, true, Enum.UserInputType.
MouseButton1)
-- правая кнопка мыши
ContextActionService:BindAction("Luch", handleAction, true, Enum.UserInputType.
MouseButton2)
Теперь возвращаемся в скрипт для Taypka и дописываем логику срабатывания события от нажатия кнопки мыши через удаленное событие (RemoteEvent). Ниже
приведен код, который нужно добавить.
raeEvent = game.ReplicatedStorage.RayLuch
-- обработка события нажатия левой кнопки мыши игроком
-- посадка тростника
raeEvent.OnServerEvent:Connect(function(Active)
-- определяем конечную точку луча
local DirVector = (script.Parent.Position + Vector3.new(0,-10,0)) —
(script.Parent.Position + Vector3.new(5, -3, 0))
-- строим луч
local LuchRay = Ray.new(script.Parent.Position + Vector3.new(-5, -3, 0),
DirVector)
local part = workspace:FindPartOnRay(LuchRay)
-- если луч определил столкновение с объектом
if part then
print("Material:", part.Material.Name)
-- список параметров предмета, которого коснулся игрок
local part, pos, normal, material =
workspace:FindPartOnRayWithWhitelist(LuchRay, {workspace.Terrain})
if material == Enum.Material.Mud then
local cloneCane = game.ReplicatedStorage.CaneClone:Clone()
cloneCane.Position = pos
cloneCane.Parent = workspace
end
end
end)
258
С помощью функции FindPartOnRayWithWhitelist мы определяем конкретные свойства
объекта, которого касается игрок.
Здесь мы обрабатываем событие пересечения луча с поверхностью земли. Нужно
учитывать то, куда направлен луч LuchRay. Для луча мы указываем начало и точку,
к которой он должен быть направлен. О свойствах луча я рассказывал в главе 3
«Платформер: структура и создание».
В примере выше мы опускаем вторую точку для луча вниз по Y на 10 studs. Эта
информация хранится в переменной DirVector.
Последние строки сравнивают полученный материал с заявленным: если они совпадают, то создается клон тростника для выращивания. Я поместил его на part,
чтобы точно позиционировать с точкой местности, назвал CaneClone и разместил
во вкладке ReplicatedStorage. Для того чтобы part с именем CaneClone смог создать
тростник, мы положим в него скрипт — он вызовет клон тростника в ту точку,
где расположен CaneClone.
Скрипт part с тростником для выращивания:
local cloneCane = game.ReplicatedStorage.Cane:Clone()
cloneCane.PrimaryPart:PivotTo(script.Parent.CFrame)
cloneCane.Parent = script.Parent
Теперь запустим игру и протестируем выращивание тростника (рис. 4.23).
РИС. 4.23. ВЫРАЩИВАНИЕ ТРОСТНИКА И ОБРАБОТКА ЗЕМЛИ
Если управление игроком стало неудобным, то кнопку мыши можно поменять
на MouseButton3 — нажатие на колесико мыши.
Следующий инструмент — кирка. Она прорубает туннели в земле вне зависимости от материала. Ее реализация схожа с алгоритмом для мотыги за исключением
того, что нам потребуется другая функция Terrain, а именно FillRegion. Эта функция
заполняет область параллелепипеда указанным материалом.
259
Если нужно копать землю, то указываем материал Air. Ниже размещен пример
скрипта для насадки Kirk. Область обработки я взял исходя из строения смоделированной мной кирки.
Если ты самостоятельно создаешь кирку, то обязательно проверь значения векторов и измени их при необходимости. Область вырезания завязана на насадке
(script.Parent.Position). Относительно ее центра строится параллелепипед
воздуха в отрицательную и положительную стороны. Обрати внимание на знаки
перед Vector3.
function TeriaMater(part)
if part.Name =="Terrain" then
--для кирки
game.Workspace.Terrain:FillRegion(Region3.new(script.Parent.Position-Vector3.
new(5,5,5), script.Parent.Position+Vector3.new(10,5,10)),4,Enum.Material.Air)
end
end
script.Parent.Touched:Connect(TeriaMater)
Пример работы кирки представлен на рис. 4.24.
РИС. 4.24. СОЗДАНИЕ ПЕЩЕРЫ
Кирка может не только копать, но и добывать полезные ископаемые. Разместим
руду с размером до (2,2,2) в ReplicatedStorage и допишем появление клонов в случайном порядке после каждого пятого удара.
260
Пример полного скрипта для кирки:
local n = 0
function TeriaMater(part)
if part.Name =="Terrain" then
n += 1 -- подсчет ударов
--для кирки
game.Workspace.Terrain:FillRegion(Region3.new(script.Parent.Position-Vector3.
new(5,5,5), script.Parent.Position+Vector3.new(10,5,10)),4,Enum.Material.Air)
local minerals = math.random(1, 5)
print(minerals)
if n>=5 then
-- случайный выбор значения от 1 до 5 включительно
if minerals ==1 then
local cloneBaselt = game.ReplicatedStorage.Basaltt:Clone()
cloneBaselt.Position = script.Parent.Position+Vector3.new(3,-3,3)
cloneBaselt.Parent =workspace
elseif minerals ==2 then
local cloneIron = game.ReplicatedStorage.IronOre:Clone()
cloneIron.Position = script.Parent.Position+Vector3.new(3,-3,3)
cloneIron.Parent = workspace
elseif minerals ==3 then
local cloneGold = game.ReplicatedStorage.GoldOre:Clone()
cloneGold.Position = script.Parent.Position+Vector3.new(3,-3,3)
cloneGold.Parent = workspace
elseif minerals ==4 then
local cloneCoal = game.ReplicatedStorage.Coal:Clone()
cloneCoal.Position = script.Parent.Position+Vector3.new(3,-3,3)
cloneCoal.Parent =workspace
elseif minerals ==5 then
print("пусто")
end
n = 0
end
end
end
script.Parent.Touched:Connect(TeriaMater)
На рис. 4.25 показан результат работы этого скрипта.
Осталось рассмотреть реализацию работы молотка — он будет создавать базальтовые кирпичи. В качестве самостоятельного задания ты можешь добавить
и доски.
Добавим молотку Part размером (4,4,4) и назовем его Create. Размести Create так,
чтобы на него смотрел молоток. Расположи созданный Part так, чтобы его основание было ниже рукояти молотка. Когда персонаж возьмет в руки молоток, то
Create долже быть расположен перед ним у поверхности земли. Это можно протестировать позже и изменять положение в режиме реального времени.
261
РИС. 4.25. ПОЛУЧАЕМ РУДУ ПРИ КОПАНИИ ЗЕМЛИ
Делаем этот объект прозрачным на 0.8 и отключаем у него всю физику вза
имодействия: CanCollide, CanQuery и CanTouch. Затем привариваем (weld) объект
к насадке молотка (рис. 4.26).
РИС. 4.26. НАСТРОЙКА МОЛОТКА
Для насадки молотка создадим скрипт, где будет обрабатываться удаленное событие нажатия — правой кнопки мыши или колесика мыши. В результате будет
создан клон кирпича размером 4 × 4 × 4 из базальта в положении зеленого куба
Create. Блок базальта — это тот же базальт, только увеличенный и не имеющий
скрипта. Назовем его Basaltt1 и разместим в ReplicatedStorage.
262
Пример скрипта:
raeEvent = game.ReplicatedStorage.RayLuch
raeEvent.OnServerEvent:Connect(function(Active)
local cloneBaselt = game.ReplicatedStorage.Basaltt1:Clone()
cloneBaselt.Position = script.Parent.Parent.Create.Position
cloneBaselt.Parent = workspace
end)
Если нужно, чтобы ориентация куба была такой же, как и у Create, используй
CFrame. На рис. 4.27 представлен пример использования инструмента молоток:
игрок создает жилище.
РИС. 4.27. СОЗДАНИЕ ДОМА С ПОМОЩЬЮ ИНСТРУМЕНТА МОЛОТОК
Осталось дописать логику завершения игры — когда игрок набрал 100 000 килограммов золота и больше. Он подходит к постаменту и становится на него. Затем
идет проверка золота на указанное значение: если оно соответствует нужному,
то ворота открываются.
Добавим скрипт на постамент:
local Players = game:GetService("Players")
function GameOver(player)
-- подключаемся к игроку и проверяем значение золота
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения монет с сервера в глобальную переменную
local MassGold = player0:WaitForChild("leaderstats").Gold.Value
if MassGold >= 100000 then
script.Parent.Parent.WoodCastle.Position -= Vector3.new(0,-100,0)
player0:WaitForChild("leaderstats").Gold.Value -= 100000
end
end
script.Parent.Touched:Connect(GameOver)
263
На рис. 4.28 показаны постамент и ворота.
РИС. 4.28. 1 — ПОСТАМЕНТ, 2 — ВОРОТА
После открытия ворот игрок подходит к красному прозрачному цилиндру с именем FinishTrigger и телепортируется в замок Кощея.
Осталось сделать тронный зал замка. Это может быть как отдельная локация на
другом place, так и удаленное место в локации. Я буду использовать второй вариант и сделаю простую комнату из камня с троном. Рядом с ним размещу свою
3D-модель Кощея, а по периметру расставлю осветительные элементы с симуляцией огня (добавлены PointLight). Также добавлю прозрачный part с именем Portal
для указания места телепортации игрового персонажа.
Пример зала представлен на рис. 4.29.
РИС. 4.29. ТРОННЫЙ ЗАЛ КОЩЕЯ
264
Пропишем логику перемещения с FinishTrigger к Portal, для этого в FinishTrigger добавим скрипт. Чтобы цилиндр привлекал игрока, сделаем его пульсирующим.
Для простоты можно вставить два скрипта: один отвечает за пульсацию, а второй
за телепортацию.
Скрипт, определяющий пульсацию триггера:
-- пульсирующий красный триггер
local SizeBool = false
while wait(0.1) do
for i = 0, 100 do
script.Parent.Size -= Vector3.new(0.02,0.02,0.02)
wait(0.005)
end
end
for j = 0, 100 do
script.Parent.Size += Vector3.new(0.02,0.02,0.02)
wait(0.005)
end
Второй скрипт — ScriptPortal:
-- телепортация персонажа
function TelePort(part)
if part.Parent:FindFirstChild("Humanoid") then
part.Parent.HumanoidRootPart.Position =
game.Workspace.GameOver.Portal.Position
end
end
script.Parent.Touched:Connect(TelePort)
На рис. 4.30 представлено расположение скриптов.
РИС. 4.30. ТРИГГЕР СО СКРИПТАМИ
Обрати внимание, что телепортация осуществляется переносом Humano
idRootPart — одной из главных частей персонажа.
В результате должно получиться примерно так, как на рис. 4.31.
Затем происходит диалог, и игра заканчивается.
265
РИС. 4.31. ТЕЛЕПОРТАЦИЯ К КОЩЕЮ
РАЗРАБОТКА ГРАФИЧЕСКОГО
ИНТЕРФЕЙСА
Теперь перейдем к написанию графического интерфейса.
Он будет состоять из меню инвентаря и всплывающего диалога с Кощеем. Инвентарь создадим по принципу магазина (см. главу 2, раздел «Экономика в играх,
игровые товары, покупка, инвентарь»).
Пример инвентаря представлен на рис. 4.32. Обрати внимание на структуру
в окне Explorer.
РИС. 4.32. СТРУКТУРА ИНВЕНТАРЯ
266
В нашем инвентаре будут отображаться все собранные ресурсы. Изображения —
это ImageButton, а поля с числом — textLabel.
Ты сможешь самостоятельно написать логику по трате накопленных ресурсов,
опираясь на всю информацию в книге.
Здесь я расскажу, как начислять количество собранных ресурсов в инвентаре
и сравнивать их с данными ресурсов у игрока. Настроив изображения, нужно добавить к Frame локальный скрипт, который будет получать данные с сервера через
удаленные события. События будут срабатывать, когда игрок собирает ресурсы.
Нужно создать события во вкладке ReplicatedStorage. Чтобы они не лежали в куче
с другими объектами, создадим для них папку Events (рис. 4.33).
Также создадим элемент Configuration в Workspace: он нужен для удобства хранения
глобальных переменных. Их можно хранить в папке, но разработчики придумали
для них специальный элемент.
В этом элементе создадим логическую переменную InventarBool, которая будет
отвечать за отображение меню инвентаря. Пример реализации представлен на
рис. 4.34.
РИС. 4.33. НАСТРОЙКА УДАЛЕННЫХ
СОБЫТИЙ
РИС. 4.34. ЛОГИЧЕСКАЯ ПЕРЕМЕННАЯ
ДЛЯ ИНВЕНТАРЯ
Теперь вернемся к локальному скрипту во Frame и пропишем код для всех иконок
и самого инвентаря.
--local remote = game.ReplicatedStorage.Events.RemoteInventar
local rem = game.ReplicatedStorage.Events.RemoteApple
local rem1 = game.ReplicatedStorage.Events.RemoteBread
local rem2 = game.ReplicatedStorage.Events.RemoteBranch
267
local
local
local
local
local
local
local
rem3
rem4
rem5
rem6
rem7
rem8
rem9
=
=
=
=
=
=
=
game.ReplicatedStorage.Events.RemoteStone
game.ReplicatedStorage.Events.RemoteCane
game.ReplicatedStorage.Events.RemoteGold
game.ReplicatedStorage.Events.RemoteIronOre
game.ReplicatedStorage.Events.RemoteCoal
game.ReplicatedStorage.Events.RemoteBasalt
game.ReplicatedStorage.Events.RemoteWood
local Run = game:GetService("RunService")
function InventVis()
if game.Workspace.Configuration.InventarBool.Value == true then
script.Parent.Visible = true
else
script.Parent.Visible = false
end
end
Run.Stepped:Connect(InventVis)
-- добавление яблок
rem.OnClientEvent: Connect (function (apple)
script.Parent.ImageApple.TextLabel.Text = tostring(apple)
end)
-- добавление плода хлебного дерева
rem1.OnClientEvent: Connect (function (bread)
script.Parent.ImageBread.TextLabel.Text = tostring(bread)
end)
-- добавление ветки
rem2.OnClientEvent: Connect (function (branch)
print("Получено")
script.Parent.ImageBranch.TextLabel.Text = tostring(branch)
end)
-- добавление камня
rem3.OnClientEvent: Connect (function (stone)
script.Parent.ImageStone.TextLabel.Text = tostring(stone)
end)
-- добавление тростника
rem4.OnClientEvent: Connect (function (cane)
print("Передача прошла")
script.Parent.ImageCane.TextLabel.Text = tostring(cane)
end)
-- добавление золота
rem5.OnClientEvent: Connect (function (gold)
script.Parent.ImageGold.TextLabel.Text = tostring(gold)
end)
-- добавление железной руды
rem6.OnClientEvent: Connect (function (ironore)
script.Parent.ImageIronOre.TextLabel.Text = tostring(ironore)
end)
-- добавление угля
rem7.OnClientEvent: Connect (function (coal)
script.Parent.ImageCoal.TextLabel.Text = tostring(coal)
end)
268
-- добавление базальта
rem8.OnClientEvent: Connect (function (basalt)
script.Parent.ImageBasalt.TextLabel.Text = tostring(basalt)
end)
-- добавление досок
rem9.OnClientEvent: Connect (function (wood)
script.Parent.ImageWood.TextLabel.Text = tostring(wood)
end)
Теперь допишем для всех ресурсов логику передачи значений через удаленное
событие. Чтобы не запутаться, скопируй названия переменных для конкретных
удаленных событий из этого скрипта.
Приведу пример полного кода для редкого тростника, ветки, базальта и золота.
Остальные скрипты к ресурсам дописываются по аналогии. Код нужно добавить
в скрипты тех ресурсов, которые расположены в ReplicatedStorage.
Редкий тростник:
local rem4 = game.ReplicatedStorage.Events.RemoteCane
local Players = game:GetService("Players")
Run = game:GetService("RunService")
function apple_money(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной CountCane
player0:WaitForChild("CountData").CountCane.Value += 1
-- отправляем через удаленное событие значение собранных тростников
local N = player0:WaitForChild("CountData").CountCane.Value
rem4:FireClient(player0, N)
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent.Active.Value = true
script.Parent.PrimaryPart.Transparency = 1
script.Parent.PrimaryPart.CanTouch = false
script.Parent.PrimaryPart.CanQuery = false
script.Parent.MeshPart1.Transparency = 1
script.Parent.MeshPart1.CanTouch = false
script.Parent.MeshPart1.CanQuery = false
script.Parent.MeshPart2.Transparency = 1
script.Parent.MeshPart2.CanTouch = false
script.Parent.MeshPart2.CanQuery = false
script.Parent.MeshPart3.Transparency = 1
script.Parent.MeshPart3.CanTouch = false
script.Parent.MeshPart3.CanQuery = false
script.Parent.MeshPart4.Transparency = 1
script.Parent.MeshPart4.CanTouch = false
script.Parent.MeshPart4.CanQuery = false
269
script.Parent.MeshPart5.Transparency = 1
script.Parent.MeshPart5.CanTouch = false
script.Parent.MeshPart5.CanQuery = false
script.Parent.MeshPart6.Transparency = 1
script.Parent.MeshPart6.CanTouch = false
script.Parent.MeshPart6.CanQuery = false
script.Parent.MeshPart7.Transparency = 1
script.Parent.MeshPart7.CanTouch = false
script.Parent.MeshPart7.CanQuery = false
script.Parent.MeshPart8.Transparency = 1
script.Parent.MeshPart8.CanTouch = false
script.Parent.MeshPart8.CanQuery = false
script.Parent.Part.Transparency = 1
script.Parent.Part.CanTouch = false
script.Parent.Part.CanQuery = false
--script.Parent.Parent:Destroy()
--script.Parent:Destroy()
end
end
script.Parent.PrimaryPart.Touched:Connect(apple_money)
local TimeStart = 0
local t =0
local n =0
-- проверяем каждый кадр значение логической переменной Active
function updateStone()
if script.Parent.Active.Value == true then
-- начало отсчета времени появления тростника
TimeStart=os.clock()
script.Parent.Active.Value=false
n =1
end
-- включение таймера
if n==1 then
t = os.clock()-TimeStart
end
-- отключение таймера
if t >=10 then
n =0
script.Parent.PrimaryPart.Transparency = 0
script.Parent.PrimaryPart.CanTouch = true
script.Parent.PrimaryPart.CanQuery = true
script.Parent.MeshPart1.Transparency = 0
script.Parent.MeshPart1.CanTouch = true
script.Parent.MeshPart1.CanQuery = true
script.Parent.MeshPart2.Transparency = 0
script.Parent.MeshPart2.CanTouch = true
script.Parent.MeshPart2.CanQuery = true
script.Parent.MeshPart3.Transparency = 0
script.Parent.MeshPart3.CanTouch = true
script.Parent.MeshPart3.CanQuery = true
270
script.Parent.MeshPart4.Transparency = 0
script.Parent.MeshPart4.CanTouch = true
script.Parent.MeshPart4.CanQuery = true
script.Parent.MeshPart5.Transparency = 0
script.Parent.MeshPart5.CanTouch = true
script.Parent.MeshPart5.CanQuery = true
script.Parent.MeshPart6.Transparency = 0
script.Parent.MeshPart6.CanTouch = true
script.Parent.MeshPart6.CanQuery = true
script.Parent.MeshPart7.Transparency = 0
script.Parent.MeshPart7.CanTouch = true
script.Parent.MeshPart7.CanQuery = true
script.Parent.MeshPart8.Transparency = 0
script.Parent.MeshPart8.CanTouch = true
script.Parent.MeshPart8.CanQuery = true
end
end
script.Parent.Part.Transparency = 0
script.Parent.Part.CanTouch = true
script.Parent.Part.CanQuery = true
t = 0
Run.Stepped:Connect(updateStone)
Ветка:
local rem2 = game.ReplicatedStorage.Events.RemoteBranch
Run = game:GetService("RunService")
local Players = game:GetService("Players")
function BranchAdd(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной CountBranch
player0:WaitForChild("CountData").CountBranch.Value += 1
-- отправляем через удаленное событие значение собранных веток
local N = player0:WaitForChild("CountData").CountBranch.Value
rem2:FireClient(player0,N)
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent.Active.Value = true
script.Parent.Transparency = 1
script.Parent.CanTouch = false
script.Parent.CanQuery = false
--script.Parent:Destroy()
end
end
script.Parent.Touched:Connect(BranchAdd)
local TimeStart = 0
local t =0
local n =0
271
-- проверяем каждый кадр значение логической переменной Active
function updateBranch()
if script.Parent.Active.Value == true then
-- начало отсчета времени появления ветки
TimeStart = os.clock()
script.Parent.Active.Value=false
n =1
end
-- включение таймера
if n==1 then
t = os.clock()-TimeStart
end
-- отключение таймера
if t >=10 then
n =0
script.Parent.Transparency=0
script.Parent.CanTouch = true
script.Parent.CanQuery = true
t = 0
end
end
Run.Stepped:Connect(updateBranch)
Базальт:
local rem8 = game.ReplicatedStorage.Events.RemoteBasalt
local Players = game:GetService("Players")
function BaseltAdd(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- увеличиваем значение переменной, подсчитывающей собранные базальты
player0:WaitForChild("CountData").CountBaselt.Value += 1
-- отправляем это значение через удаленное событие
local N = player0:WaitForChild("CountData").CountBaselt.Value
rem8:FireClient(player0,N)
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent:Destroy()
end
end
script.Parent.Touched:Connect(BaseltAdd)
Золото:
local rem5 = game.ReplicatedStorage.Events.RemoteGold
local Players = game:GetService("Players")
function GoldAdd(player)
-- подключаемся к игроку и добавляем значение
local player0 = Players:GetPlayerFromCharacter(player.Parent)
-- присвоение значения глобальной переменной Gold
player0:WaitForChild("leaderstats").Gold.Value+=1
-- присваиваем значение золота через удаленное событие в инвентарь
local N = player0:WaitForChild("leaderstats").Gold.Value
rem5:FireClient(player0, N)
272
if player and player.Parent:FindFirstChild("Humanoid") then
script.Parent:Destroy()
end
end
script.Parent.Touched:Connect(GoldAdd)
Инвентарь пока не обрабатывается удаленно с клиента, а считывает глобальную
переменную InventarBool. Включать и отключать инвентарь будем с помощью
клавиши F. Для этого достаточно дописать код в локальный скрипт в папке
StarterPlayerScripts.
Вот полный код этого скрипта:
Run = game:GetService("RunService")
local remote = game.ReplicatedStorage.Events.RemoteInventar
local rayEvent = game.ReplicatedStorage.RayLuch
---- определяем локального игрока
local player = game:GetService("Players").LocalPlayer
wait(2)
local player0 = player.Character:WaitForChild("Humanoid")
---- находим анимацию
local anim = game.ReplicatedStorage.Animation
----подключаем анимацию к игроку
local anim0 = player0:LoadAnimation(anim)
anim0.Looped = false
local ContextActionService = game:GetService("ContextActionService")
local BoolVis = false
-- функция по работе с событиями от пользователя
local function handleAction(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
if actionName == "Sword" then
print("Удар")
anim0:Play()
end
if actionName == "Luch" then
rayEvent:FireServer(true)
end
if actionName == "Invent" then
print("BoolVis = ", BoolVis)
end
end
BoolVis = not(BoolVis)
remote:FireServer(BoolVis)
end
-- левая кнопка мыши
ContextActionService:BindAction("Sword", handleAction, true,
Enum.UserInputType.MouseButton1)
-- средняя кнопка мыши
ContextActionService:BindAction("Luch", handleAction, true,
Enum.UserInputType.MouseButton3)
-- клавиша F
ContextActionService:BindAction("Invent", handleAction, true, Enum.KeyCode.F)
273
При вызове элемента кода Invent происходит отправка на сервер значения логической переменной BoolVis. Код BoolVis = not(BoolVis) — это логическое
отрицание (инверсия). Его смысл в том, что присваивается противоположное
значение к уже имеющемуся. Если было true, то станет false и наоборот.
Теперь это удаленное событие со значением нужно принять на сервере. Для этого
создадим скрипт с именем InventarScript в Workspace и пропишем такие строки:
local remote = game.ReplicatedStorage.Events.RemoteInventar
remote.OnServerEvent:Connect(function(player, Logic)
game.Workspace.Configuration.InventarBool.Value = Logic
end)
Протестируем работу инвентаря. Предварительно в окне Properties укажи false
для параметра Visible элемента Frame.
Пример реализации представлен на рис. 4.35.
РИС. 4.35. РАБОТА ИНВЕНТАРЯ
Теперь осталось доработать монолог Кощея и указать отображения двух кнопок:
синяя — возвращаемся домой;
красная — остаемся жить на острове.
Если игрок нажимает синюю кнопку, его подбрасывает вверх, где он останавливается: игра закончена. Во втором случае Кощей исчезает, а игрок продолжает
жить на острове.
Создадим еще один Frame с именем DialogFrame, в котором будут два TextButton
и один TextLabel. Для трех первых элементов добавим по локальному скрипту
(рис. 4.36).
274
РИС. 4.36. ДИАЛОГ КОНЦОВКИ ИГРЫ
Во вкладке ReplicatedStorage создадим папку FinishEvent, а в ней два удаленных события с именами ActiveDialog и CastleEvent.
Для локального скрипта, принадлежащего DialogFrame, добавим следующие
строки.
local eventActive = game.ReplicatedStorage.FinishEvent.ActiveDialog
eventActive.OnClientEvent:Connect(function(BoolVis)
script.Parent.Visible = true
script.Parent.TextLabel.Text = "Ты прошел испытания великого собирателя
и попал в замок Кощея. Теперь у тебя есть выбор: либо ты остаешься, либо
отправляешься домой"
end)
Диалог отобразится после срабатывания удаленного события ActiveDialog. Оно
сработает, когда игрок коснется FinishTrigger, и поэтому в скрипт ScriptPortal нужно дописать логику передачи значений через удаленное событие при касании.
Ниже полный код этого скрипта:
local eventActive = game.ReplicatedStorage.FinishEvent.ActiveDialog
local Players = game:GetService("Players")
-- телепортация персонажа
function TelePort(part)
if part.Parent:FindFirstChild("Humanoid") then
local player0 = Players:GetPlayerFromCharacter(part.Parent)
part.Parent.HumanoidRootPart.Position =
game.Workspace.GameOver.Portal.Position
eventActive:FireClient(player0, true)
end
end
script.Parent.Touched:Connect(TelePort)
Предварительно нужно снять с параметра Visible галочку для DialogFrame.
275
Теперь перейдем к написанию логики для кнопок. Для кнопки Home можно
создать part и разместить удаленно от острова, чтобы переместить туда игрока
и создать иллюзию полета.
Скрипт для кнопки Home будет выглядеть так:
---- определяем локального игрока
local player = game:GetService("Players").LocalPlayer
function Home()
local player0 = player.Character:WaitForChild("Humanoid")
if player0 then
script.Parent.Parent.Visible =false
player.Character.HumanoidRootPart.Position =
game.Workspace.Home.Position +Vector3.new(0,5,0)
player.Character.Humanoid.WalkSpeed =0
end
end
script.Parent.Activated:Connect(Home)
Персонаж перемещается в позицию part с именем Home. Чтобы игрок не упал,
у него отключается скорость передвижения.
Следующий шаг — сделать локальный скрипт для кнопки Castle.
local remCastle = game.ReplicatedStorage.FinishEvent.CastleEvent
function Castle()
script.Parent.Parent.Visible =false
remCastle:FireServer(true)
end
script.Parent.Activated:Connect(Castle)
При нажатии кнопки Остаться сработает удаленное событие на сервер. Это событие убирает Кощея. Создадим для его модели скрипт, где обработаем это событие.
local remCastle = game.ReplicatedStorage.FinishEvent.CastleEvent
remCastle.OnServerEvent:Connect(function(player, BoolVis)
if BoolVis == true then
script.Parent:Destroy()
end
end)
Скрипт лежит внутри модели.
Теперь доработаем логику выхода из замка. Стена за троном послужит порталом,
который перебросит игрока в точку SpawnLocation.
Ниже код скрипта для стены-портала:
local Players = game:GetService("Players")
-- телепортация персонажа
function TelePort(part)
if part.Parent:FindFirstChild("Humanoid") then
276
local player0 = Players:GetPlayerFromCharacter(part.Parent)
part.Parent.HumanoidRootPart.Position = game.Workspace.SpawnLocation.
Position + Vector3.new(0,10,0)
end
end
script.Parent.Touched:Connect(TelePort)
На рис. 4.37 показано меню с текстом и кнопками.
РИС. 4.37. ВЫБОР ИГРОКА
На этом игра закончена.
Надеюсь, что информация о жанрах платформера и песочницы была тебе полезна. Все игры можно постепенно дорабатывать и улучшать, и с каждым шагом
доработки ты будешь совершенствовать свое мастерство.
В играх также популярны магазины и продажа различных товаров. Поэтому
в следующей главе рассмотрим варианты создания товаров и их реализацию.
5
СОЗДАНИЕ
ИГРОВОГО
МАГАЗИНА
В этой главе рассмотрим варианты монетизации как игры,
так и своих моделей. Научимся создавать внутриигровые
предметы для покупки их за Robux и рассмотрим варианты
создания и продажи своей одежды в маркетплейсе Roblox.
278
Заключительная глава книги посвящена монетизации. Она происходит через
создание товаров как для сообщества в целом, так и для их использования внутри
игрового мира. Монетизация важна тем, что помогает разработчикам получать
материальную и моральную поддержку.
Пока ты новичок, то о каком-то большом заработке в сообществе Roblox можно не задумываться. На момент написания книги в сообществе насчитывается
12 миллионов разработчиков игр и более 45 миллионов игр. Зарегистрировано
около 200 миллионов пользователей, из которых ежедневно активно 20 миллионов человек.
Конкуренция в Roblox-сообществе высокая. Потенциальный игрок не видит
одновременно все 45 миллионов игр, поэтому платформа предлагает ему игры
по критериям его личного выбора: то, во что он обычно любит играть, заходя
в игровое пространство.
Второй фактор распространения игры — реклама. В первую очередь игроку
будут предлагать самые популярные игры с оплаченной рекламой. Чем крупнее
компания, тем больше она вкладывает денег в рекламу своего продукта. Поэтому
в любой игровой среде начинающему инди-разработчику нужно сформировать
сообщество потенциальных игроков: вести блог или канал по игре, предлагать
бонусы первым игрокам, проводить розыгрыши призов среди подписчиков,
делиться полезной информацией по играм или их созданию.
Если ты хочешь, чтобы о твоей игре узнали, нужно о ней говорить. Один из популярных способов — это ведение группы в соцсети и канала на видеохостинге.
Также о своей игре ты можешь писать на форумах или тематических сайтах.
Можно писать о самой игре, о ее механике или дизайне и показывать примеры.
Примеры соцсетей и тематических сайтов:
Социальные сети: VK, Telegram, TikTok и др.
Видеохостинги и стриминговые хостинги: RuTube, YouTube, Twitch, Good Game,
Trovo.
Тематические сайты и форумы: DefFoum Roblox, Roblox Studio Developer Форум
Ru, VC, DTF, Хабр.
Выбери наиболее понравившиеся и начинай продвигать свой проект.
А теперь поговорим о монетизации. Я немного затронул ее во второй главе, посвященной структуре игр, а далее расскажу о ней подробно.
279
ИГРОВАЯ ВАЛЮТА: ВНУТРЕННЯЯ
И ГЛОБАЛЬНАЯ
В играх можно создать внутриигровую валюту или использовать деньги сообщества Roblox, которые называются Robux. Далее я покажу распространенные
варианты использования технологий для зарабатывания Robux.
Robux можно конвертировать из долларов и обратно. Курс зависит от курса доллара, поэтому точную сумму можно узнать только во время обмена. В личном
кабинете ты можешь посмотреть, какая сумма в Robux у тебя есть (рис. 5.1).
РИС. 5.1. СУММА ROBUX
На этой странице курс обмена Robux (рис. 5.2).
РИС. 5.2. КУРС ROBUX К ДОЛЛАРУ
Проводить операции могут только владельцы банковских карт и, следовательно,
финансовых счетов. Поэтому подобные операции доступны только пользователям
в возрасте от 14 лет.
А как же обменять валюту на доллары, то есть вывести Robux на свой счет? Сделать это сложнее. На странице https://create.roblox.com/devex приведены требования
для вывода средств (рис. 5.3).
280
РИС. 5.3. ТРЕБОВАНИЯ ДЛЯ ВЫВОДА СРЕДСТВ
Пользователь должен быть старше 13 лет, иметь на счету от 50 000 Robux
и обладать хорошей репутацией (не публиковать запрещенный материал, не
оскорблять других участников сообщества и т. д.). Электронная почта подтверждается уже при регистрации, поэтому это требование не должно вызвать
вопросов.
Также есть пункт о действительности учетной записи портала DevEx. И здесь,
возможно, потребуется подтверждение личности. Как правило, для этого нужно
сделать фото с удостоверением личности. В некоторых случаях могут попросить
оформить премиум-подписку. Но если твой контент сделан хорошо и пользуется
спросом, то модераторы могут автоматически поставить значок проверенного
разработчика.
Не каждый разработчик доходит до заветной суммы для вывода средств, о чем
часто пишут на форуме Roblox: https://devforum.roblox.com/. Но это не значит, что
заработать нельзя. Возможно, именно у тебя и получится создать прекрасную
игру и полезные товары, которые будут пользоваться большим спросом. Если
ты соберешь портфолио с привлекательными проектами и товарами, то тобой
могут заинтересоваться работодатели, в том числе и крупные компании, которые
могут оплачивать твой труд как Robux, так и реальным деньгами.
Другой вариант — это донаты. Их могут делать как в самом проекте, так и на
стриминговых сервисах, где ты будешь рассказывать о своих разработках.
Сначала варианты заработка ограниченны. В самом Roblox можно зарабатывать
на дизайне одежды для игроков или продавать авторские игровые элементы в самой игре. Они могут быть постоянные или временные. Постоянные покупаются
один раз и навсегда, а временные активны ограниченное время, затем их нужно
покупать снова.
Можно создавать платные плагины или платные игры (режимы). Также ты можешь размещать бесплатно 3D-модели, звуки, текстуры и изображения, mesh
281
3D-объектов и анимацию. Продавать их можно только после того, как ты подтвердишь аккаунт и получишь статус проверенного разработчика. Поэтому в этой
главе сосредоточимся на товарах, которые может продавать новичок.
Возможно, в твоих проектах появится собственная валюта, которая поможет вовлечь пользователей в игру, но и тогда эта валюта может быть связана с Robux.
Например, в игре есть дорогие полезные предметы, которые покупают либо за
большое количество игровой валюты, либо только за Robux. Примеры внутри
игровой валюты мы уже рассматривали в книге.
Гейм-дизайн внутриигровой валюты:
настроить в игре целесообразность использования валюты: какую роль она
играет, степень ее важности;
продумать экономику игры: стоимость товаров и их динамику, процесс зачислений и накоплений, степень доступности товаров;
возможность пополнять внутриигровую валюту за Robux.
РАЗРАБОТКА ТОВАРОВ ДЛЯ ПРОДАЖИ
В СООБЩЕСТВЕ ROBLOX
Теперь приступим к созданию товаров, которые можем продавать: рубашек,
штанов и футболок. Эти товары может создать и продать любой начинающий
разработчик. Документацию ищи здесь: https://create.roblox.com/docs/avatar/
accessories/classic-clothing.
Согласно правилам Roblox, чтобы разместить такой товар в маркетплейсе, нужно
заплатить комиссию в 10 Robux за каждый товар.
Для начала разместим футболку T-Shirt. Мы можем взять любое изображение,
которое отвечает правилам сообщества Roblox: https://en.help.roblox.com/hc/enus/articles/203313410-Roblox-Community-Standards. Зададим размер изображения
512 × 512 пикселей. Для примера я взял фрактал, который сделала моя супруга
в своей программе (рис. 5.4).
Теперь нужно протестировать футболку на потенциальных гуманоидах пользователей. Создадим новый проект в Roblox Studio и опубликуем его как Test Shirt.
Затем во вкладке View откроем окно Asset Manager. Если проект не опубликован,
то в этом окне попросят провести эту операцию.
Здесь будет представлено несколько папок. Нас интересует папка Image: переходим в нее и загружаем через инструмент Bulk Import отредактированное изображение. Затем загружаем основной набор шаблонов гуманоидов через вкладку
AVATARRig Build. Я выбираю стандарт R15. Подгрузим Block Rig, Mesh Rig, Man Rig,
Woman Rig. При желании можно загрузить и другие шаблоны.
282
РИС. 5.4. ПРИМЕР ИЗОБРАЖЕНИЯ ДЛЯ ФУТБОЛКИ
Каждому из них добавим элемент Shirt Graphic и в свойствах параметра Graphic
выберем загруженное изображение (рис. 5.5).
РИС. 5.5. ДОБАВЛЯЕМ ИЗОБРАЖЕНИЕ К ЭЛЕМЕНТУ ТКАНИ
283
После того как все готово, можно проверить, насколько гармонично картинка
смотрится на каждом шаблоне (рис. 5.6).
РИС. 5.6. ТЕСТИРОВАНИЕ ФУТБОЛОК НА ШАБЛОНАХ
Если изображение загружено с помощью Asset Manager, то напротив файла могут
появиться красные или оранжевые значки. Они указывают на статус проверки
изображения на соответствие правилам сообщества. Оранжевый означает, что
рисунок проверяется, а красный — что он не прошел проверку. В этом случае
тебе в личный кабинет пришлют уведомление, что изображение не отвечает правилам сообщества. Если после загрузки файла никаких значков нет, это значит,
что изображение одобрено.
Теперь загрузим футболки в Avatar Shop. Для этого перейдем во вкладку Create
в личном кабинете Roblox и щелкнем по вкладке T-Shirt. Выберем изображение
и нажмем Upload (рис. 5.7).
Чтобы назначить цену и добавить описание товара, щелкни по значку шестеренки напротив изображения. Появится меню из двух пунктов. Выбери Configure
(рис. 5.8).
284
РИС. 5.7. ЗАГРУЗКА ФУТБОЛКИ
РИС. 5.8. НАСТРОЙКА ТОВАРА
285
Чтобы задать цену, выбери вкладку Sales. Активируй поле Item for Sale: появится
инструмент настройки цены товара. Когда ты укажешь цену и нажмешь Save,
тебя попросят заплатить 10 Robux. После этого товар размещается в Avatar Shop
во вкладке Clothing — Classic T-Shirts (рис. 5.9). Товар можно найти по названию.
Если ты отказываешься от уплаты комиссии, то товар не размещают в магазине.
РИС. 5.9. ФУТБОЛКА В МАГАЗИНЕ ДЛЯ АВАТАРОВ
Футболка появится в твоем профиле и в инвентаре. Теперь любой желающий
может ее купить за указанную цену, а тебе придет 70 % от стоимости покупки.
РИС. 5.10. ПОЛЬЗОВАТЕЛЬ ПРИМЕРЯЕТ ФУТБОЛКУ
Таким же образом разместим рубашку (верхнюю часть костюма) и брюки (по
умолчанию брюки и футболка). Размещение почти такое же, но создание чуть
сложнее.
286
Перейдем во вкладку Shirts и загрузим шаблон с выкройкой рубашки (рис. 5.11).
РИС. 5.11. ЗАГРУЖАЕМ ШАБЛОН ВЫКРОЙКИ РУБАШКИ
Щелкни по фразе download it here: откроется страница с шаблоном (рис. 5.12).
Скачай его и не меняй размер в редакторе.
РИС. 5.12. ШАБЛОН ВЫКРОЙКИ РУБАШКИ
На шаблоне указано, как располагаются детали рубашки на персонаже. На эти
цветные области нужно разместить свои изображения. Для этого можно использовать любой графический редактор: Paint, PhotoShop, GIMP, Inkscape и т. д.
На рис. 5.13 и 5.14 представлены примеры расположения изображений.
287
РИС. 5.13. НАКЛАДЫВАЕМ ИЗОБРАЖЕНИЯ
РИС. 5.14. ПРИМЕР РАСПОЛОЖЕНИЯ
НА ШАБЛОН
ИЗОБРАЖЕНИЯ НА ШАБЛОНЕ
Полученную выкройку нужно сохранить в формате .png или .jpeg. Теперь протестируем одежду на шаблонных гуманоидах. Для этого добавим элемент Shirt (он
добавится к гуманоиду с именем Clothing), а изображение подгрузим в параметр
ShirtTemplate. Если результат понравился, то переходим в CreateShirts и загружаем
созданную одежду за 10 Robux. Далее добавляем описание товара и указываем
стоимость.
Аналогичным способом создаются и брюки. Для начала нужно перейти во вкладку
Pants и скачать шаблон для брюк (рис. 5.15).
На рис. 5.16 приведен пример.
РИС. 5.15. ШАБЛОНЫ БРЮК И ФУТБОЛКИ РИС. 5.16. СОЗДАНИЕ БРЮК И ФУТБОЛКИ
288
Обязательно протестируй их на шаблонах гуманоидов (рис. 5.17). Для этого добавь элемент Pants, а изображение подгрузи параметру PantsTemplate.
РИС. 5.17. ТЕСТИРОВАНИЕ РУБАШЕК И БРЮК
Если одежда понравилась, то размещаем ее через вкладку Pants и платим 10 Robux.
После того как ты укажешь цену товаров, можешь добавить их в свой профиль
и примерить. Чтобы проверить их наличие в магазине Avatar Shop, перейди на
вкладку ClothingClassic.
На размещение трех товаров мы потратили 30 Robux. Если не рекламировать
и не показывать другим игрокам свои товары, то вероятность покупки невелика.
Рекламировать товары ты можешь бесплатно в своих играх, соцсетях или видео
хостингах, а платно — через Roblox. На странице каждого товара есть элемент
для оплаты рекламы. Используй этот инструмент, если будешь уверен, что он
окупится.
ПРОДАЕМ ТОВАРЫ ВНУТРИ ИГРЫ
ЗА ROBUX
В главе 2 «Игра: структура и технологии» мы учились создавать внутриигровую
валюту и проводить с ее помощью оплату. Мы также создали магазин для проведения больших сделок. Теперь же добавим в магазин товары, которые можно
приобрести за Robux. Такими товарами может стать созданная одежда или некий
игровой опыт, который сделает игру интереснее.
Откроем новый проект в Roblox Studio. Там мы создадим игровые объекты,
которые будут давать улучшения игровому персонажу, если он купит их внутри
289
игры за Robux. Для примера я взял созданную ранее игровую модель Tele_Portal.
Дополнительно создадим волшебную палочку, которая будет изменять цвет предмета. Палочка будет стрелять лучами.
Создадим инструмент Wand так, чтобы его можно было брать в руки. Он будет
состоять из самой палочки (magicWand), рукояти (Handle) и прицела (Cel). На
рис. 5.18 бирюзовый part — это Cel, белый part внутри палочки у основания — это
Handle, а разноцветный part с набалдашником — это magicWand.
РИС. 5.18. ВОЛШЕБНАЯ ПАЛОЧКА
В этот инструмент добавим скрипт, где пропишем логику запуска луча (рис. 5.19).
РИС. 5.19. СТРУКТУРА WAND
Ниже представлена логика скрипта. Она почти такая же, как у пушки. При
щелчке левой кнопкой мыши мы отправляем луч определенной длины. Если
он встречает препятствие (какой-либо part), то меняет ему цвет на случайный
(седьмая строка). Чтобы можно было видеть луч, создадим маленькие parts
разного цвета, которые будут перемещаться по лучу. В конце созданные parts
нужно уничтожить.
290
function RayMouse()
print(game.Workspace.Pos.Value)
local vecPos = 300*game.Workspace.Pos.Value.Unit
local DirVector = vecPos - script.Parent.Cel.Position
local LuchRay = Ray.new(script.Parent.Handle.Position, DirVector)
local part = workspace:FindPartOnRay(LuchRay)
part.Color = Color3.new(math.random(),math.random(),math.random())
print("Hit part: " .. part.Name)
game.Workspace.Pos.Value = part.Position
local DirVector1 =part.Position - script.Parent.Cel.Position
print(DirVector1.Magnitude)
game.Workspace.Active.Value =true
if DirVector1.Magnitude>=500 then
DirVector1.Magnitude = 500
end
for i =1, math.ceil(DirVector1.Magnitude) do
local sfer = Instance.new("Part",script.Parent)
sfer.CanCollide = false
sfer.Material = Enum.Material.Neon
sfer.Anchored = true
sfer.Size = Vector3.new(0.2,0.2,0.2)
sfer.Position=script.Parent.Cel.Position+2*i*LuchRay.Direction.Unit
sfer.CFrame= CFrame.new(sfer.Position)
sfer.Color = Color3.new(math.random(),math.random(),math.random())
sfer.Name = "Sfer"..tostring(i)
wait(0.01)
end
for i =1, math.ceil(DirVector1.Magnitude) do
local sf=script.Parent["Sfer"..tostring(i)]
sf:Destroy()
wait(0.01)
end
end
game.Workspace.Active.Value =false
script.Parent.Activated:Connect(RayMouse)
Разместим инструмент Wand в ReplicatedStorage.
Суперспособностью будет увеличение скорости ходьбы на 30 studs в секунду.
Создадим модель магазина, чтобы игрок сразу понимал, что может купить здесь
товары (рис. 5.20). Разместим копии моделей созданных инструментов. Для
супербега добавим модель гуманоида. Товары нельзя брать в руки, поэтому
скрипты и Handle здесь удалены.
Здесь же мы можем создать part в виде стены для тестирования купленных предметов — пушки и палочки. Пушка будет растворять стену (см. логику пушки
в игре-платформере).
291
РИС. 5.20. МАГАЗИН
Теперь нужно опубликовать игровую сцену и перейти во вкладку HomeGame
Settings. В открывшемся окне перейдем в пункт Monetization и создадим три продукта разработчика: это разделы для разграничения и продажи конкретной
модели или опыта (рис. 5.21).
РИС. 5.21. СОЗДАЕМ ПРОДУКТЫ РАЗРАБОТЧИКА
292
Для созданных шаблонов нужно указать название продукта и его цену в Robux.
После сохранения настроек продукту присвоится уникальный ID. Этот номер
понадобится для настройки скрипта по продаже товаров.
Для витрины магазина я создал три разноцветных кубика, на которых размещены предметы и опыт. У кубиков есть элемент ClickDetector для взаимодействия
с мышью. Внутри размещены меши моделей без скриптов, а сами кубики прозрачные (рис. 5.22).
РИС. 5.22. СТРУКТУРА МОДЕЛИ МАГАЗИНА
Модели товаров размещены в папке ReplicatedStorage (рис. 5.23).
РИС. 5.23. МОДЕЛИ ТОВАРОВ
Теперь перейдем к написанию скрипта продажи, который разместим в модели магазина. В этом примере я разместил его в Shop и сделал дочерними кубики S1 и S2.
293
Подключимся к службе онлайн-магазина Roblox MarketplaceService. Затем пропишем ID продуктов разработчика и их функции. В них укажем, что эти товары
дадут игроку. Список ID продуктов хранится в таблице. Замени в коде фразу ID
продукта на реальные ID в своем проекте.
-- подключаемся к сервису онлайн-магазина Roblox
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
-- создаем таблицу для хранения ID продуктов
local productFunctions = {}
-- прописываем ID продуктов и их назначение
productFunctions[ID продукта1] = function(player)
-- если купим у Shop, то получим волшебную палочку
local cloneCube = game.ReplicatedStorage.Wand:Clone()
cloneCube.Parent = player.Backpack
end
-- если купим у S1, то увеличим скорость на 30
productFunctions[ID продукта2 ] = function(player)
player.Character.Humanoid.WalkSpeed += 30
end
-- если купим у S2, то получим растворитель стен
productFunctions[ID продукта3] = function(player)
local cloneTele = game.ReplicatedStorage.Tele_Port:Clone()
cloneTele.Parent = player.Backpack
end
Теперь пропишем функции: они будут продавать игроку товары по ID. В дальнейшем функции будут вызываться щелчком мыши. Для этого понадобится
ClickDetector.
-- функции продажи товаров
function pod(player)
print(player)
MarketplaceService:PromptProductPurchase(player, ID продукта1)
end
function slap1(player)
print(player)
MarketplaceService:PromptProductPurchase(player, ID продукта2)
end
function slap2(player)
print(player)
MarketplaceService:PromptProductPurchase(player, ID продукта3)
end
Поскольку продажа — это очень ответственный процесс, то нужно сформировать
чек и проверить успешность покупки. Для этого понадобится еще одна функция
из документации MarketplaceService.
-- Функция для создания чека и проверки транзакции
local function processReceipt(receiptInfo)
print(receiptInfo.PurchaseId) -- ID покупки
print(receiptInfo.PlayerId) --ID игрока
print(receiptInfo.ProductId) --ID продукта
print(receiptInfo.CurrencySpent) -- количество потраченных Robux
294
print(receiptInfo.CurrencyType) -- тип валюты (Robux)
print(receiptInfo.PlaceIdWherePurchased) -- ID Place (игры), где прошла оплата
end
-- определяем игрока, который решил купить товар
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
if not player then
warn ("Ошибка при покупке: Игрок не найден")
return Enum.ProductPurchaseDecision.NotProcessedYet
end
-- продаем найденному игроку товар из списка
local succes = pcall(productFunctions[receiptInfo.ProductId], player)
if not succes then
warn("Ошибка при покупке: Функция завершилась неудачей")
return Enum.ProductPurchaseDecision.NotProcessedYet
end
-- завершаем сделку в случае успеха
return Enum.ProductPurchaseDecision.PurchaseGranted
-- вызов функции для формирования чека и транзакции
MarketplaceService.ProcessReceipt = processReceipt
Информация, которую выдает функция print, нужна нам для проверки продажи.
Теперь пропишем вызов функции продажи по щелчку на товаре.
-- продажа волшебной палочки
script.Parent.ClickDetector.MouseClick:Connect(pod)
-- продажа ускорения
script.Parent.S1.ClickDetector.MouseClick:Connect(slap1)
-- продажа растворителя стен
script.Parent.S2.ClickDetector.MouseClick:Connect(slap2)
Опубликуем игру, чтобы сохранить изменения на сервере и проверить работу
скрипта. Ниже на рис. 5.24–5.26 представлены покупки трех товаров по щелчку
на них.
РИС. 5.24. ПРОДАЖА СКОРОСТНОГО РЕЖИМА
295
РИС. 5.25. ПРОДАЖА ВОЛШЕБНОЙ ПАЛОЧКИ
РИС. 5.26. ПРОДАЖА РАСТВОРИТЕЛЯ СТЕН
Если покупка успешная, то товары окажутся в рюкзаке игрока и игрок начнет
быстро перемещаться. В тестовом режиме в Roblox Studio плата за покупку не
производится, но чек формируется. Купить товары по-настоящему можно в опубликованной игре.
При тестировании покупки в окне Output можно увидеть результат сделки
(рис. 5.27).
РИС. 5.27. ОТЧЕТ
296
Также в твоем личном кабинете во вкладке с финансами отобразится информация
о совершенной покупке (рис. 5.28).
РИС. 5.28. ТРАНЗАКЦИИ
Ты можешь закрепить покупку за игроком, записав данные в DataStore (см. раздел «Игровой опыт» в главе 2). В этом случае, когда игрок будет заходить в твою
игру, у него всегда будет в наличии купленный товар.
В примере выше у игрока есть товары до тех пор, пока он находится в игре — после выхода он потеряет их.
Результат покупки — на рис. 5.29.
РИС. 5.29. ИСПЫТАНИЕ ВОЛШЕБНОЙ ПАЛОЧКИ
ПОСЛЕСЛОВИЕ
Вот и подошла к концу эта книга. Я постарался как можно подробнее структурировать и передать информацию о том, как разрабатывать игры в Roblox.
Создание и продвижение игр требует множества умений и навыков. Опираясь
на эту информацию, тебе будет легче идти путем разработчика. Получив знания
и отточив навыки по моим книгам, ты сможешь разработать и опубликовать
разные игры. Главное — внимательность, усердие и желание. Тогда тебе покорятся вершины!
Приведенные в книге модели, сюжет, алгоритмы на 99 % созданы мной (с опорой
на документацию). Многие модели из книги доступны по ID, их можно найти по
ссылке на мой GitHub https://github.com/Antipat/Game-Dev-Roblox.
Рекомендую тебе создавать свой неповторимый контент. Такой путь занимает
больше времени, но дает уникальный опыт: ты сможешь побыть сценаристом,
художником, 3D-художником, программистом, маркетологом, тестировщиком
и не только.
Создавай свои игры, собирай портфолио и рекламируй свои достижения. Желаю
успехов в разработке игр!
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ
A
AlignOrientation 72
AlignPosition 72
AlwaysOnTop 175
Anchor 70
AngularVelocity 72, 171
Animation 62
AnimationController 142
Animation Editor 57
Animations 60
Arhive 15
Atmosphere 29
Attachment 74
AVATAR 57
Avatar Importer 141
Avatar Shop 285
B
BackgroundTransparency 107
BindAction 42
BoolValue 37
Brightnes 80
C
Camera 24
CameraMaxZoomDistance 24
CameraMinZoomDistance 24
CameraMode 24
CanCollide 45
CFrame 71
CFrame.Angles 72
Classic 24
ClickDetector 35
Clothing 285
CollisionFidelity 127
ContextActionService 40
Custom 135
Cylindrical 49
Density 29
DevCameraOcclusionMode 24
DevComputerCameraMovementMode 24
DevEx 280
E
Enum 255
Enum.UserInputState 41
Enum.UserInputState.Begin 41
Explorer 15
F
Face 175
FillRegion 258
FindPartOnRayWithWhitelist 258
Fire 49
FireAllClients 151
FireClient 150
FireServer 151
Frame 107
Fuse 138
G
GamepadKeyCode 44
Game Setting 223
Glare 29
Group As a Model 129
GUI 105
H
Handle 87
Haze 29
Health 46
Hinge 49
HoldDuration 44
Home 15
Humanoid 35
Humanoid:LoadAnimation 242
D
I
DataStoreService 82
Decay 29
ID 61
ImageButton 151
299
ImageLabel 107
In Place 140
Insert Object 17
Inventory 165
InvokeServer 151
K
KeyboardKeyCode 44
KeyCode 43
L
leaderstats 95
Lighting 29
LinearVelocity 72
LineForce 72
LocalScript 40
LockFirstPerson 24
M
match 196
Material 19, 255
Material Manager 165
math.rad 72
MaxActivationDistance 44
MaxForce 74
MeshId 135
MeshPart 135
Mixamo 138
Model 16
ModuleScript 57
Monetization 291
Motor6D 64
My Games 14
N
Negate 126
New 13
NumberValue 82
numpy 116
O
ObjectText 44
Offset 29
OnClientEvent 151
OnServerInvoke 151
Open Cloud 82
OpenCV 116
os 116
Output 19
P
Pants 287
ParticleEmitter 77
pillow 116
PivotTo 72
Play 221
Players 97
Plugins 17
PointLight 80
PresizeConvexDecomposition 127
PrimaryPart 34
Properties 15
ProximityPrompt 43
Publish to Roblox 222
Python 116
R
R15 57
Range 81
Ray 187
Recent 14
Region3 255
Remote Events 149
ReplaceMaterial 255
ReplicatedStorage 54
Rig Build 57
RigityEnabled 74
Roblox 8
Robux 279
Run 221
RunService 42
S
Sales 285
Screenshot 105
ServerScriptService 45
Shirts 286
ShirtTemplate 287
Sign up 8
Smoke 49
Solid Modeling 122
SpotLight 80
Start 221
StarterGui 107
StarterPlayer 23
StarterPlayerScripts 40
StringValue 174
Submit 60
SurfaceLight 80
300
T
И
Terrain 255
Terrarian Editor 114
Test 17
TextLabel 107
Texture Maps 165
Toolbox 19
Torque 72
Transparency 45
T-Shirt 281
Игра 22
Инструмент 87
U
UnbindAction 42
Upload Character 139
UserPartColor 122
V
Values 82
VectorForce 72
View 17
W
WalkSpeed 85
Weld 49
К
Камера 23
Карта высот 115
Ключи анимации 59
Конфликт 163
М
Матрица 71
Матрица поворота 71
Меш 34
О
Опорная точка 70
П
Песочница 225
Платформер 162
Подсчета системного времени 197
Публикация модели 137
Р
А
Аватар 10
Аккаунт 8
Анимация персонажа 59
Атмосфера игры 29
В
Видеохостинги 278
Г
Генерация карт 114
Д
Двумерная таблица 118
З
Завязка 163
Звуки 100
Развязка 163
Рассказ 162
С
Сборная модель 34
Сбор предметов 33
Свет 25
Социальные сети 278
Сценарий 162
Т
Таблица 132
Тематические сайты 278
Товары 143
Триггер 42
Ц
Цвет 25
Целочисленная переменная 51
Андрей Корягин
Roblox в действии. Искусство разработки игр
Руководитель дивизиона
Ведущий редактор
Литературный редактор
Художественный редактор
Корректоры
Верстка
Ю. Сергиенко
Е. Строганова
К. Тульцева
В. Мостипан
С. Беляева, Г. Шкатова
Л. Егорова
Изготовлено в России. Изготовитель: ООО «Прогресс книга».
Место нахождения и фактический адрес: 194044, Россия, г. Санкт-Петербург,
Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373.
Дата изготовления: 04.2024. Наименование: книжная продукция. Срок годности: не ограничен.
Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12 — Книги печатные
профессиональные, технические и научные.
Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01.
Подписано в печать 06.03.24. Формат 84×108/16. Бумага офсетная. Усл. п. л. 31,920. Тираж 1200. Заказ 0000.
Бренда Ромеро, Ян Шрайбер
ИГРОВОЙ БАЛАНС.
ТОЧНАЯ НАУКА ГЕЙМДИЗАЙНА
В сфере игрового дизайна балансировка является чем-то вроде черной
магии. Данный процесс позволяет геймдизайнеру сделать игру честной
по отношению к игрокам и обеспечить им как раз ту степень сложности,
которая делает процесс увлекательным и достаточно хардкорным, не
давая игре стать слишком предсказуемой. Это требует одновременного
использования математики, психологии, а иногда и знаний из других
областей, например экономики и теории игр.
КУПИТЬ
Стив Инс
КАК СОЧИНИТЬ ВИДЕОИГРУ:
201 СОВЕТ ОТ СЦЕНАРИСТА
BROKEN SWORD И RESIDENT EVIL
Разработка сценария для видеоигры — увлекательное дело. Но игровым
писателям приходится постоянно учиться и бросать вызов ограничениям.
Это связано с тем, что игровая индустрия развивается и становится все
более зрелой. Тут и стремительное совершенствование технологий,
которое мы наблюдаем из года в год, и растущие ожидания игроков.
Написание текста — нелегкий труд, в особенности когда речь идет
об игровом сценарии. Необходимость выдать нужное качество, не
нарушая дедлайнов, еще больше усложняет задачу. Помощь никогда не
будет лишней.
Перед вами книга, в которой собран 201 легко усваиваемый совет
от легендарного геймдизайнера.
КУПИТЬ
Евгений Романенко
BLENDER. ДИЗАЙН ИНТЕРЬЕРОВ
И АРХИТЕКТУРЫ
Откройте для себя удивительный мир 3D-графики. Начните самостоятельно
изучать основы 3D-моделирования и визуализации с помощью Blender. Действуйте
уже сейчас!
Blender уже завоевал мир. Его выбирают дизайнеры и художники, ведь в их
распоряжении оказывается огромный набор мощных инструментов моделинга,
текстурирования, анимации и рендеринга.
Если вы новичок и только решили попробовать себя в области 3D-дизайна, это
руководство именно для вас. В этой книге вы познакомитесь с интерфейсом,
научитесь пользоваться всеми базовыми инструментами для качественного
3D-моделинга, визуализации и экспорта готовой работы.
Если же вы опытный художник-конструктор и хотите добавить в свой набор
компетенций еще и Blender, то сможете быстро разобраться в нюансах работы
с пакетом.
Получите полезные знания и практические советы на примере конкретных
задач, куда входят: промышленный 3D-дизайн, основы композиции, качественная
топология модели, развертка и текстура, настройка материалов (шейдеров),
постановка света, создание окружения.
КУПИТЬ