🇷🇺
MyWebAR Knowledge Base
Язык Ру
Язык Ру
  • База знаний для разработчиков MyWebAR
  • С ЧЕГО НАЧАТЬ
    • Регистрация
    • Обзор страницы Dashboard
    • Обзор редактора
    • Создание проекта в MyWebAR
  • ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ MYWEBAR
    • Создание сцены с трекингом на изогнутых изображениях
    • Создание AR-визитки
    • Создание портала в MyWebAR
    • Оживающая фотография
    • Интерактив с 3D-моделью
  • ПЛАНЫ И ПОДПИСКИ
    • Бесплатная пробная подписка
    • Обновление вашего плана
    • Коммерческие планы
    • Планы для образования
  • СОЗДАНИЕ ПРОЕКТОВ WEBAR
    • Типы проектов
    • Проекты с мультимаркерным трекингом (книги)
    • Добавление объектов
    • Как добавить 3D-модель из библиотеки моделей
    • Как добавить 3D-модель из Sketchfab
    • Свойства объекта
    • Кнопки и действия
    • Поведения объектов
    • Воспроизведение видео
    • 3D Анимации
    • Аналитика проектов
    • Как сделать хорошее отслеживаемое изображение для дополненной реальности
    • Оптимизация и подготовка 3D-моделей к загрузке
    • Брендирование проекта и его настройки
    • Работа с доступными плагинами
    • Существующие плагины и как с ними работать
    • Видео инструкции
  • КАСТОМИЗАЦИЯ WEBAR
    • Кастомный домен
    • Использование внешнего хранилища
    • Встраивание WebAR
  • Pro Editor
    • Как устроен Pro Editor
      • Описание интерфейса
      • Основные возможности
    • Требования по размещению кода
      • Пример интеграции готового скрипта
      • Создание частиц в Pro Editor
      • Работа с видео
      • Работа с анимациями 3D-объектов
    • Текущие ограничения
      • Работа с камерой
      • Создание UI
      • Загрузка объектов с помощью класса loader
      • Импорт частей кода
    • Кейсы
      • Добавление изображений
      • Эффект бликов на объективе (Lens Flare)
      • Пошаговое создание мини-игры
      • Переключение содержимого по нажатию
      • Пошаговое создание квеста
Powered by GitBook
On this page
  • Подготовка к созданию
  • Добавление объектов на сцену
  • Игровое поле
  • Предметы для поиска
  • Шаблоны-ориентиры
  • Главный персонаж
  • Стрелка-подсказка
  • Работа с кодом
  • Основная механика игры
  • Работа со звуком
  • Работа с анимациями главного персонажа
  • Функционал стрелки-подсказки
  • Эффект биллборда у объектов
  • Экспорт сцены в MyWebAR
  1. Pro Editor
  2. Кейсы

Пошаговое создание мини-игры

PreviousЭффект бликов на объективе (Lens Flare)NextПереключение содержимого по нажатию

Last updated 2 years ago

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

В данном случае будет создана игра типа . На сцене будут разбросаны предметы-объекты, и игроку предстоит найти определенные из них.

Отсканируйте QR-код и посмотрите на сцену, которая получится в результате.

В данном случае мы создадим только шаблон мини-игры. Предметами для поиска будут обычные разноцветные плоскости (Plane).

Чтобы придать шаблону проекта реалистичный вид, можно заменить цветные карточки реальными объектами (добавив текстуры или заменив их 3D-объектами).

Подготовка к созданию

Таким образом, создание игры начнется с чистого проекта.

Добавление объектов на сцену

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

  1. Игровое поле (стена и пол),

  2. Разбросанные предметы на игровом поле (разноцветные плоскости),

  3. Шаблоны, показывающие объект, который нужно искать (шаблоны, соответствующие разноцветным плоскостям),

  4. Главный персонаж (3D-объект),

  5. Стрелка-подсказка.

Игровое поле

Для создания игрового поля будут использованы две обычные плоскости в крупном масштабе (одна из которых будет повернута на 90 градусов).

Чтобы добавить плоскость, нажмите Add → Plane.

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

Далее, нужно увеличить размер плоскостей. В данном случае параметр Scale = 14.0, 14.0, 0.0.

Перещаем вертикальную плоскость немного назад. Вторую плоскость поворачиваем на -90 градусов.

Параметры стены:

Position: 0.0, 10.0, -10.0. Rotation: 0.0, 0.0, 0.0. Scale: 20.0, 20.0, 0.0.

Параметры пола:

Position: 0.0, 0.0, 0.0. Rotation: -90.0, 0.0, 0.0. Scale: 20.0, 20.0, 0.0.

Чтобы объекты на сцене не были черными, нужно просто добавить два источника света. В данном случае Ambient и Directional lights.

Предметы для поиска

Следующим шагом будет добавление самих объектов в сцену. В данном случае это будут просто разноцветные плоскости.

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

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

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

Чтобы добавить группу, нажмите Add → Group.

Добавленная группа появится в дереве объектов справа (но изменений на самой сцене не будет).

Группа обязательно должна находиться на нулевых позициях, т.е. Position: 0.0, 0.0, 0.0. Если этого не сделать, предметы могут оказаться в неправильном месте.

Также стоит дать группе другое имя, чтобы не потерять ее в списке всех объектов сцены. Для этого нужно изменить значение параметра Name, выделив группу.

Создание предметов

Следующим шагом будет создание элементов. Для этого нажмите Add → Plane.

Объект появится на сцене.

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

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

Далее необходимо перетащить созданную плоскость в группу.

Следующий шаг — немного увеличить размер плоскости. Scale: 1.5, 1.5, 1.0 и немного поднять вверх (так, чтобы край плоскости касался поверхности пола: обычно это значение равно половине масштаба, то есть 0.75).

Теперь нам нужно расположить предмет на сцене, это может быть любое место на полу.

Следующим шагом мы изменим цвет плоскости. Для этого необходимо перейти на вкладку Material.

Здесь, рядом с полем Color, нажмите на блок с цветом. Далее в палитре выберите нужный цвет (например, красный).

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

Подобным образом мы создадим еще 19 предметов.

После этого, можно приступать к созданию шаблонов-ориентиров.

Шаблоны-ориентиры

Шаблоны — очень важный элемент игр Hidden Object. Благодаря им игрок понимает, какой предмет ему нужно найти.

В данном случае мы создадим пять шаблонов.

Создание группы

Создадим группу так же, как и при создании предметов (смотрите в подглаве Создание группы для предметов).

Группу мы назовем "GroupOfTemplates".

Создание объектов

Так же, как и в случае с объектами, вам нужно создать плоскость, нажав Add → Plane.

Далее мы помещаем эту плоскость в группу GroupOfTemplates.

Цвет плоскости оставим прежним. Текстурой шаблона будем управлять с помощью кода.

Далее нам нужно увеличить ее размер. Затем переместим плоскость в переднюю часть пола (перед предметами) и повернем лицевой стороной вверх.

Таким образом, параметры каждого из шаблонов:

  • Шаблон номер 1: Position: -5.0, 0.02, 8.5. Rotation: -90.0, 0.0, 0.0. Scale: 2.0, 2.0, 1.0.

  • Шаблон номер 2: Position: -2.5, 0.02, 8.5. Rotation: -90.0, 0.0, 0.0. Scale: 2.0, 2.0, 1.0.

  • Шаблон номер 3: Position: 0.0, 0.02, 8.5. Rotation: -90.0, 0.0, 0.0. Scale: 2.0, 2.0, 1.0.

  • Шаблон номер 4: Position: 2.5, 0.02, 8.5. Rotation: -90.0, 0.0, 0.0. Scale: 2.0, 2.0, 1.0.

  • Шаблон номер 5: Position: 5.0, 0.02, 8.5. Rotation: -90.0, 0.0, 0.0. Scale: 2.0, 2.0, 1.0.

В результате сцена с шаблонами будет выглядеть следующим образом (для лучшей видимости шаблонов цвет был изменен на черный):

Главный персонаж

В этой сцене главным героем является обычная случайная 3D-модель, которая содержит несколько анимаций.

Добавление 3D-модели ничем не отличается от стандартного добавления, то есть вы нажимаете File → Import.

Далее, выбрав файл, он появится на сцене. В данном случае это будет просто динозавр.

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

После этого нужно разместить модель в правом углу пола, в позиции: Position: 8.75, 0.0, 8.5.

Стрелка-подсказка

Следующим шагом будет добавление стрелки-подсказки. Это будет обычное изображение стрелки.

Для стрелки нужно просто добавить плоскость, как обычно нажав Add → Plane.

Плоскость на сцене.

Далее необходимо перейти на вкладку Material.

Напротив пункта Map нужно нажать на прямоугольник, чтобы добавить текстуру стрелки (также нужно установить флажок рядом с прямоугольником).

Теперь у плоскости появлась текстура стрелки.

Далее необходимо добавить прозрачность.

Для этого нужно прокрутить вниз вкладку Materials и найти пункт Transparent.

Установка флажка сделает стрелку прозрачной.

Теперь, немного подправив размер, стрелка готова.

Также необходимо скрыть стрелку по умолчанию, сняв флажок visible на вкладке Object.

Работа стрелки будет описана ниже, в пункте Функционал стрелки-подсказки.

Работа с кодом

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

Сам код делится на определенные блоки:

  1. Основная механика. Здесь будет находиться функционал самой игры.

  2. Работа с звуком. Воспроизведение звуков реакции персонажа на действия игрока.

  3. Работа с анимацией. Запуск анимации персонажа, который будет реагировать на действия игрока.

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

Основная механика игры

Основная механика игры заключается в случайной активации определенных объектов (всего 5 из 20 созданных). Активные объекты кликабельны. При правильном клике объекты и детали меняют цвет.

Итак, первое, что нужно сделать, это создать скрипт, привязанный к сцене, нажав на кнопку New.

Далее необходимо перейти в редактор кода, нажав на кнопку Edit.

Теперь можно приступить непосредственно к написанию кода.

Первое, что нужно сделать, это создать две переменные, которые будут содержать уже созданные GroupOfObjects и GroupOfTemplates (строки 1-2).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

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

Для начала я создам пустой объект (строка 4).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};

Далее я создам цикл с двадцатью итерациями (количество предметов для поиска).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
	}
}

Этот цикл заполнит пустой объект предметами. Для этого я добавлю два свойства внутри переменной element: active (активен ли объект) и object (ссылка на объект в сцене) (строки 7-8).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
}

Теперь нам нужно сделать так, чтобы созданный элемент был добавлен в созданный objectList (строка 10).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

Теперь объект objectList будет содержать элементы, соответствующие объектам в сцене.

Далее нам нужно сделать так, чтобы из списка объектов выбиралось только пять (равное количеству шаблонов-ориентиров). Для этого первым делом нужно создать функцию, которая будет возвращать случайные числа (строки 13-21).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber();

Теперь внутри массива threeRandNumb будет 5 случайных чисел. Далее из списка объектов на сцене нужно выбрать отдельные 5 и добавить их в отдельный массив, а затем работать отдельно только с ними.

Для этого мы создадим еще один массив arrOffActiveObjs и функцию, которая, используя случайные числа, будет добавлять их в arrOffActiveObjs (строки 23-27).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

Теперь в массиве arrOffActiveObjs будет пять отдельных объектов.

Следующий шаг — активировать эти объекты. Для этого мы создадим функцию с массивом arrOffActiveObjs и двумя циклами (строки 29-37).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {

	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {

	}	
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {

	}
}

Внутри перебора будет происходить замена флага active с false на true (строки 30-33).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
	
	}	
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {

	}
}

Первый цикл поможет связать активные объекты с шаблонами-ориентирами, включая управление текстурой (строки 35-39).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {

	}
}

Второй цикл добавит внутрь самих предметов свойство active (строки 41-43).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}
}

Теперь нам нужно запустить функцию activateObject. Для этого внутри функции init (это стандартная функция редактора, которая запускается по умолчанию) нужно вызвать только что созданную функцию (строки 46-48).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}		
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}
}

function init() {
	activateObjects();
}

Теперь, когда мы запускаем сцену (при нажатии кнопки Play), шаблоны-ориентиры будут окрашены в случайные цвета, которые будут соответствовать предметам на сцене.

Последний шаг — сделать объекты в сцене кликабельными. Для этого мы будем использовать встроенный в threejs класс Raycasting.

Сам Raycaster имеет стандартный шаблон (строки 50-61).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}		
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}
}

function init() {
	activateObjects();
}

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( scene.children, true);
	if (intersects.length > 0) {
	
	}
}

В 53 строке, вместо scene.children, нужно добавить массив объектов, по которым мы планируем нажимать.

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

Кубы должны быть созданы в том же месте, что и плоскости, и с теми же размерами. Чтобы сделать кубы невидимыми, необходимо изменить параметр visibility (строки 50-63).

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}		
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}
}

function init() {
	activateObjects();
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	//objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( scene.children, true);
	if (intersects.length > 0) {
	
	}
}

Теперь, чтобы Raycaster работал по кубами, вместо scene.children в 72 строке нужно написать boxGroup.children.

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}		
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}
}

function init() {
	activateObjects();
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	//objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0) {
	
	}
}

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

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}		
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}
}

function init() {
	activateObjects();
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	//objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		
	}
}

Далее добавим код для изменения цвета объекта и соответствующего ему рисунка, а также деактивации щелкнутого объекта (строки 75-77).


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}		
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}
}

function init() {
	activateObjects();
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	//objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
	}
}

Теперь, чтобы активировать Raycasting, создадим функцию start (эта функция запускается в определенный момент самим редактором). И внутри нее добавим слушатель события onMouseDown (строки 1-3)

function start() {
	document.addEventListener( 'mousedown', onMouseDown, false );
}

const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
		
	
	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}
}

function init() {
	activateObjects();
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	//objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
	}
}

Работа со звуком

На данный момент, из-за закрытости браузера Safari, работа со звуком требует больше усилий, чем просто создание AudioListener.

Для начала я добавлю шаблон в самом начале кода игровой механики.

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {};

const init_promises = [];

const sound_urls = {
	sound1: 'url_to_sound',
	sound2: 'url_to_sound',
	sound3: 'url_to_sound',
};


function soundSetVolume(name, volume) {
    const sound = sounds[name];
    sound.volume = volume;
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv());
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start() {
	paused = false;
	Promise.all(init_promises).then(() => {
		document.addEventListener('mousedown', onMouseDown, false);
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function stop() {
	paused = true;
}

function mywebar_fill_init_promises(promises) {
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias) {
	medias.push(...Object.values(sounds));
}

function init(json, init_callbacks = {}) {
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;

	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}

Нет смысла разбирать работу этого шаблона. Вместо этого ниже мы рассмотрим, как его использовать.

Первое, что необходимо сделать, это добавить ссылки на аудиофайлы, которые вы хотите воспроизвести. Ссылки должны быть добавлены внутри объекта, расположенного на строках 9-13.

const sound_urls = {
	sound1: 'url_to_sound',
	sound2: 'url_to_sound',
	sound3: 'url_to_sound',
};

Допустим, вы хотите добавить три аудиофайла. Первый будет воспроизводиться при неправильном ответе, второй — при правильном ответе, а третий — при прохождении игры.

Мы можем изменить имена ключей свойств. Названия изменятся следующим образом: sound1 → wrong, sound2 → right, sound3 → win.

const sound_urls = {
	wrong: 'url_to_sound',
	right: 'url_to_sound',
	win: 'url_to_sound',
};

Теперь вместо url_to_sound мы должны добавить ссылку на соответствующие аудиофайлы.

Таким образом, наш объект будет выглядеть, например, так:

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};

Следующим шагом будет непосредственно запуск аудио. Для этого в данном шаблоне есть специальная функция sound_play(name).

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

Таким образом, чтобы запустить, например, звук wrong, нужно написать следующую строчку кода: sound_play(wrong);.

Добавим внутри рейкастинга (функция onMouseDown), внутрь if, следующую строчку:

Следующий шаг — запуск самого звука. Для этого в данном шаблоне предусмотрена специальная функция sound_play(name).

То есть, чтобы запустить звук, нужно вызвать эту функцию и передать в качестве параметра имя аудиофайла, которое мы задали выше. Таким образом, чтобы запустить, например, sound_play('right');, нужно написать следующую строку кода. Добавим следующую строку внутри функции onMouseDown (внутри if, оответствующего нажатию на правильном предмете):

sound_play('right');

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

Теперь добавим еще одно условие, внутри Raycasting.

else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {

}

Что здесь происходит? Если объект, на котором щелкнул игрок, неактивен (то есть неправильный), то срабатывает код внутри условия.

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

else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
    sound_play('wrong');
}

Теперь, если мы нажмем не на тот объект, будет воспроизведен соответствующий звук.

ВАЖНО: Убедитесь, что ваш код содержит только одну функцию start, init и update. Если у вас есть более одного использования этой функции, звук не будет работать!

Неправильно:

function start() {

//something with audio

}

//somecode

function start() {

//using the START function again

}

Правильно:

function start() {

//something with audio

//other code

}

//somecode

Последним шагом будет аудио со звуком победы в игре (win).

Сначала добавим две переменные (строки 139-140):

  1. let oneTimeFlag = false; — простой флаг, который будет менять свое значение.

  2. let checkArr = []; — пустой массив.

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}

let oneTimeFlag = false;
let checkArr = [];

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		console.log('inheritedToy', intersects[0].object.inheritedObject);
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		sound_play('right');
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}

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

Добавим еще одну строку, которая добавит число 1 (важно не число, добавленное в массив, а количество элементов в нем) в массив checkArr строка 171).

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}

let oneTimeFlag = false;
let checkArr = [];

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		console.log('inheritedToy', intersects[0].object.inheritedObject);
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		sound_play('right');
		checkArr.push(1);
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}

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

Далее мы создаем функцию update(). Это стандартная функция редактора, которая выполняется каждый кадр (строки 141-143).

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}

let oneTimeFlag = false;
let checkArr = [];
function update() {

}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		console.log('inheritedToy', intersects[0].object.inheritedObject);
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		checkArr.push(1);
		sound_play('right');
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}

Внутри функции необходимо создать перебор массива объектов на сцене (предметов) и проверять каждый элемент.

Если объект неактивен (!elem.active), флаг oneTimeFlag == false и количество объектов в массиве (если элементов в массиве пять, то есть найдены все пять объектов - checkArr.length == 5) — запускается аудио со звуком победы, а флаг(oneTimeFlag) меняет свое значение на false (строки 143-152).

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}

let oneTimeFlag = false;
let checkArr = [];
function update() {

	groupOfObjects.children.forEach(elem => {
		if (!elem.active && !oneTimeFlag && checkArr.length == 5) {
			oneTimeFlag = true;
			sound_play('win');
		}
	});

}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		console.log('inheritedToy', intersects[0].object.inheritedObject);
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		checkArr.push(1);
		sound_play('right');
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}

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

  • При нажатии на правильный объект,

  • При нажатии на неправильный объект,

  • При прохождении игры.

Работа с анимациями главного персонажа

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

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

let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
  1. Часы, для работы с анимациями.

  2. Главный персонаж.

  3. Первая дорожка с анимацией у главного персонажа.

  4. Вторая дорожка с анимацией у галвного персонажа.

  5. Миксер, служащий для запуска анимаций.

  6. Анимационный клип для первой анимации.

  7. Анимационный клип для второй анимации.

Далее сделаем так, чтобы вторая анимация всегда останавливалась на последнем кадре (строка 8) и отключим зацикливание (строка 9).

let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;

Теперь нужно запустить первую анимацию (строка 10).

let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

Чтобы анимация заработала, необходимо добавить условие (строки 11-15) в уже созданную функцию update().

let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();
function update(event) {
	if (mixer) {
		mixer.update( event.delta/1000 );
	}
}

Примечание: Как вы можете видеть, в строке 13 в качестве параметра mixer.update передается event.delta/1000. Поскольку дельта времени отличается в редакторе PRO и в MyWebAR, необходимо добавить дополнительные строки кода.

Для правильной настройки delta, просто добавьте следующие строчки:

  1. var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function'; — перед созданием переменных.

  2. if (isMae) delta /= 1000; if(!inited) return; if(typeof(Helper) === 'undefined') event.delta /= 1000; — внутри функции update().

Полный код с учетом данных исправлений (строки 12 и 14-22).

let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function';
function update(event) {		
	delta = event.delta;
    	if (isMae) delta /= 1000;
	if(!inited) return;

	if(typeof(Helper) === 'undefined') event.delta /= 1000;
	
	if (mixer) {
		mixer.update(delta);
	}
}

Теперь анимация будет правильно работать как в расширенном редакторе, так и в самом MyWebAR.

Следующим шагом будет добавление анимации-реакции на победу в игре (когда все предметы найдены).

В конце подраздела было создано условие внутри функции update(), которое срабатывает после того, как игрок нашел все предметы на сцене. Внутри этого условия можно добавить код для запуска определенной анимации. Эта анимация запускается вместе со звуком победы (строки 36-39).

let oneTimeFlag = false;
let checkArr = [];
let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function';
function update(event) {
		
	delta = event.delta;
    if (isMae) delta /= 1000;
	if(!inited) return;

	if(typeof(Helper) === 'undefined') event.delta /= 1000;
	if (mixer) {
		mixer.update( delta );
	}
	
	
	groupOfObjects.children.forEach(elem => {
		if (!elem.active && !oneTimeFlag && checkArr.length == 5) {
			oneTimeFlag = true;
			sound_play('win');
			actionforClipOne.setEffectiveTimeScale( 1 );
			actionforClipOne.setEffectiveWeight( 1 );
			actionforClipOne.stop();
			actionforClipTwo.play();
		}
	});
}

Полный код сцены, включая звук, механику и анимацию:

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}


let oneTimeFlag = false;
let checkArr = [];
let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function';
function update(event) {
		
	delta = event.delta;
    	if (isMae) delta /= 1000;
	if(!inited) return;

	if(typeof(Helper) === 'undefined') event.delta /= 1000;
	if (mixer) {
		mixer.update( delta );
	}
	
	groupOfObjects.children.forEach(elem => {
		if (!elem.active && !oneTimeFlag && checkArr.length == 5) {
			oneTimeFlag = true;
			sound_play('win');
			actionforClipOne.setEffectiveTimeScale( 1 );
			actionforClipOne.setEffectiveWeight( 1 );
			actionforClipOne.stop();
			actionforClipTwo.play();
		}
	});
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		console.log('inheritedToy', intersects[0].object.inheritedObject);
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		sound_play('right');
		checkArr.push(1);
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}

Функционал стрелки-подсказки

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

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

Первое, что нужно сделать, это создать переменную, которая будет содержать объект GroupOfObjects.

let groupOfObjects = scene.getObjectByName('groupOfObjects');

Внутри редактора создадим функцию idle. Внутри этой функции будет цикл, который будет перебирать предметы на сцены (строки 3-7).

let groupOfObjects = scene.getObjectByName('groupOfObjects');

function idle() {
	for (let key in groupOfObjects.children) {

	}
}

Внутри цикла нужно добавить проверку объекта на наличие active: true (строки 6-8).

let groupOfObjects = scene.getObjectByName('groupOfObjects');

function idle() {
	for (let key in groupOfObjects.children) {
		if (groupOfObjects.children[key].active) {

		}
	}
}

Далее, внутри условия задаем положение стрелки (строки 6-7).

let groupOfObjects = scene.getObjectByName('groupOfObjects');

function idle() {
	for (let key in groupOfObjects.children) {
		if (groupOfObjects.children[key].active) {
			this.position.x = groupOfObjects.children[key].position.x;
			this.position.z = groupOfObjects.children[key].position.z;
		}
	}
}

Теперь стрелка будет появляться всегда над активным объектом.

Появление стрелки

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

Далее, над функцией update(), нужно создать новую переменную totalTime = 0 (строка 140). Эта переменная будет накапливать время с момента последнего нажатия. А также создать переменную arrow, которая будет хранить саму стрелку из сцены (строка 141).

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}


let totalTime = 0;
let arrow = scene.getObjectByName("Arrow");
let oneTimeFlag = false;
let checkArr = [];
let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function';
function update(event) {
		
	delta = event.delta;
    if (isMae) delta /= 1000;
	if(!inited) return;

	if(typeof(Helper) === 'undefined') event.delta /= 1000;
	if (mixer) {
		mixer.update( delta );
	}
	
	groupOfObjects.children.forEach(elem => {
		if (!elem.active && !oneTimeFlag && checkArr.length == 5) {
			oneTimeFlag = true;
			sound_play('win');
			actionforClipOne.setEffectiveTimeScale( 1 );
			actionforClipOne.setEffectiveWeight( 1 );
			actionforClipOne.stop();
			actionforClipTwo.play();
		}
	});
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		checkArr.push(1);
		sound_play('right');
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}

Далее, внутри функции update() необходимо выполнить операцию инкремента (строка 167).

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}


let totalTime = 0;
let arrow = scene.getObjectByName("Arrow");
let oneTimeFlag = false;
let checkArr = [];
let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function';
function update(event) {
		
	delta = event.delta;
    	if (isMae) delta /= 1000;
	if(!inited) return;

	if(typeof(Helper) === 'undefined') event.delta /= 1000;
	if (mixer) {
		mixer.update( delta );
	}
	
	totalTime += delta;
	
	groupOfObjects.children.forEach(elem => {
		if (!elem.active && !oneTimeFlag && checkArr.length == 5) {
			oneTimeFlag = true;
			sound_play('win');
			actionforClipOne.setEffectiveTimeScale( 1 );
			actionforClipOne.setEffectiveWeight( 1 );
			actionforClipOne.stop();
			actionforClipTwo.play();
		}
	});
	
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		checkArr.push(1);
		sound_play('right');
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}

Далее создадим условие, которое будет проверять количество секунд с момента последнего нажатия. Если значение totalTime будет больше этого времени, стрелка появится, в противном случае она будет невидимой (строки 168-171).

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}


let totalTime = 0;
let arrow = scene.getObjectByName("Arrow");
let oneTimeFlag = false;
let checkArr = [];
let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function';
function update(event) {
		
	delta = event.delta;
    	if (isMae) delta /= 1000;
	if(!inited) return;

	if(typeof(Helper) === 'undefined') event.delta /= 1000;
	if (mixer) {
		mixer.update( delta );
	}
	
	totalTime += delta;
	arrow.visible = false;
	if (totalTime > 6) {
		arrow.visible = true;
	}
	
	groupOfObjects.children.forEach(elem => {
		if (!elem.active && !oneTimeFlag && checkArr.length == 5) {
			oneTimeFlag = true;
			sound_play('win');
			actionforClipOne.setEffectiveTimeScale( 1 );
			actionforClipOne.setEffectiveWeight( 1 );
			actionforClipOne.stop();
			actionforClipTwo.play();
		}
	});
	
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		checkArr.push(1);
		sound_play('right');
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}

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

Для этого внутри Raycasting будем переопределять переменную totalTime(строка 210).

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}


let totalTime = 0;
let arrow = scene.getObjectByName("Arrow");
let oneTimeFlag = false;
let checkArr = [];
let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function';
function update(event) {
		
	delta = event.delta;
    	if (isMae) delta /= 1000;
	if(!inited) return;

	if(typeof(Helper) === 'undefined') event.delta /= 1000;
	if (mixer) {
		mixer.update( delta );
	}
	
	totalTime += delta;
	arrow.visible = false;
	if (totalTime > 6) {
		arrow.visible = true;
	}
	
	groupOfObjects.children.forEach(elem => {
		if (!elem.active && !oneTimeFlag && checkArr.length == 5) {
			oneTimeFlag = true;
			sound_play('win');
			actionforClipOne.setEffectiveTimeScale( 1 );
			actionforClipOne.setEffectiveWeight( 1 );
			actionforClipOne.stop();
			actionforClipTwo.play();
		}
	});
	
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		totalTime = 0;
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		checkArr.push(1);
		sound_play('right');
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}

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

Также стоит добавить удаление стрелки по окончанию игры (строка 175).

let paused = true;
let inited = false;
let started = false;
let sounds = {};
const sound_autoplay = {
};

const init_promises = [];

const sound_urls = {
	right: 'https://mywebar-a.akamaihd.net/11882/41404/Right.aac',
	wrong: 'https://mywebar-a.akamaihd.net/11882/41404/Wrong.aac',
	win: 'https://mywebar-a.akamaihd.net/11882/41404/Win.aac'
};
function soundSetVolume(name, volume){
    const sound = sounds[name];
    sound.volume = volume
}

function sound_play(name) {
	for(const [name, sound] of Object.entries(sounds)){
		console.log('[SOUND] stop', name, sound);
		sound.pause();
		sound.currentTime = 0;
	}
	const sound = sounds[name];
	let promise = sound.play();
	if(!promise) promise = new Promise(rv => rv()); // safari
	console.log('[SOUND] play', name, sound);
	return promise;
}


function start(){
	paused = false;
	Promise.all(init_promises).then(() => {
		activateObjects();
		document.addEventListener( 'mousedown', onMouseDown, false );
		inited = true;
		if(!paused && !started){
			started = true;
		}
	});
}

function init(json, init_callbacks = {}){
	const {
		loadSound = (url, {autoplay = false}) => new Promise(rv => {
			const a = new Audio;
			a.autoplay = autoplay;
			a.muted = false;
			a.crossOrigin = "anonymous";
			a.src = url;
			a.oncanplay = () => {
				a.oncanplay = undefined;
				rv(a);
			};
			a.load();
		}),
	} = init_callbacks;
	
	Object.entries(sound_urls).forEach(([name, url]) => {
		const promise = loadSound(url, {autoplay: !!sound_autoplay[name]})
			.then(sound => {
				if(sound_autoplay[name]){
					sound.pause();
					sound.currentTime = 0;
				}
				console.log('[SOUND] load', name, sound);
				sounds[name] = sound;
			})
		;
		init_promises.push(promise);
	});
}



function stop(){
	paused = true;
}

function mywebar_fill_init_promises(promises){
	promises.push(...init_promises);
}

function mywebar_fill_medias(medias){
	medias.push(...Object.values(sounds));
}


const groupOfObjects = this.getObjectByName('GroupOfObjects');
const groupOfTemplates = this.getObjectByName('GroupOfTemplates');

const objectList = {};
for ( let index = 0; index < 20; index++ ) {
	const element = {
		active: false,
		object: groupOfObjects.children[index]
	}
	objectList[ `object${index + 1}` ] = element;
}

let threeRandNums = [];
function makeRandumNumber( length = 5 ) {
	threeRandNums = [];
	while(threeRandNums.length < length){
		let r = Math.floor(Math.random() * 20) + 0;
		if(threeRandNums.indexOf(r) === -1) threeRandNums.push(r);
	}
}
makeRandumNumber()

let arrOffActiveObjs = [];
function setupActivesObj() {
	arrOffActiveObjs = threeRandNums.map( randomNum => Object.values(objectList)[randomNum] );
}
setupActivesObj();

function activateObjects() {

	for (let i = 0; i < groupOfObjects.children.length; i++) {
		groupOfObjects.children[i].active = false;
	}

	
	arrOffActiveObjs.forEach( elem => {
		elem.active = true;
		elem.object.active = true;
	});

	for (let i = 0; i < arrOffActiveObjs.length; i++) {
		let template = groupOfTemplates.children[i];
		groupOfTemplates.children[i].material.color = arrOffActiveObjs[i].object.material.color;
		arrOffActiveObjs[i].object.inheritedTemplate = template;
	}	
}


let totalTime = 0;
let oneTimeFlag = false;
let checkArr = [];
let arrow = scene.getObjectByName('Arrow');
let clock = new THREE.Clock();
const dino = scene.getObjectByName('dino');
const clipOne = dino.animations[0];
const clipTwo = dino.animations[1];
const mixer = new THREE.AnimationMixer(dino);
const actionforClipOne = mixer.clipAction(clipOne);
const actionforClipTwo = mixer.clipAction(clipTwo);
actionforClipTwo.clampWhenFinished = true;
actionforClipTwo.loop = THREE.LoopOnce;
actionforClipOne.play();

var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function';
function update(event) {
		
	delta = event.delta;
    if (isMae) delta /= 1000;
	if(!inited) return;

	if(typeof(Helper) === 'undefined') event.delta /= 1000;
	if (mixer) {
		mixer.update( delta );
	}
	
	totalTime += delta;

	if (totalTime > 6) {
		arrow.visible = true;
	}
	
	groupOfObjects.children.forEach(elem => {
		if (!elem.active && !oneTimeFlag && checkArr.length == 5) {
			scene.remove(arrow);
			oneTimeFlag = true;
			sound_play('win');
			actionforClipOne.setEffectiveTimeScale( 1 );
			actionforClipOne.setEffectiveWeight( 1 );
			actionforClipOne.stop();
			actionforClipTwo.play();
		}
	});
}

let boxGroup = new THREE.Group();
boxGroup.name = 'boxGroup';
for (let i = 0; i < groupOfObjects.children.length; i++) {
	let objBox = new THREE.Mesh(
		new THREE.BoxGeometry( 1.5, 1.5, 1.5 ),
		new THREE.MeshBasicMaterial( {color: 0x000000, visible: false} )
	);
	objBox.position.copy(groupOfObjects.children[i].position);
	objectList.objectBox = objBox;
	objBox.inheritedObject = groupOfObjects.children[i];
	objBox.name = `Box ${ i + 1 }`;
	boxGroup.add(objBox);
}
scene.add(boxGroup);

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseDown( event ) {
	event.preventDefault();
	mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
	mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
	raycaster.setFromCamera( mouse, camera );
	let intersects = raycaster.intersectObjects( boxGroup.children, true);
	if (intersects.length > 0 && intersects[0].object.inheritedObject.active) {
		totalTime = 0;
		intersects[0].object.inheritedObject.active = false;
		intersects[0].object.inheritedObject.material.color = new THREE.Color( 0x000000 );
		intersects[0].object.inheritedObject.inheritedTemplate.material.color = new THREE.Color( 0x000000);
		sound_play('right');
		checkArr.push(1);
	} else if (intersects.length > 0 && !intersects[0].object.inheritedObject.active) {
   		sound_play('wrong');
	}
}175

Эффект биллборда у объектов

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

const _obj = this.getObjectByName(this.name);
var active = true;
function init() {
	_obj.rotation.order = "YXZ";
}
function update(event) {
	if (!active) return;
	const pos = new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z);
	_obj.up.set(0,1,0);
	_obj.lookAt(pos);
	_obj.rotation.z = 0;
}

Этот код должен быть добавлен ко всем объектам, которые должны смотреть в камеру. В случае этой мини игры, объектами с данным эффектом будут: Стрелка-подсказка, Главный персонаж и Предметы для поиска.

В качестве примера ниже показано, как создать подобное для стрелки.

Для этого необходимо создать еще один новый скрипт (чтобы разделить сценарии), нажав на Add.

Мы должны написать название скрипта, чтобы было понятно, что этот скрипт отвечает за вращение объекта.

После этого нажмем EDIT. В открывшемся редакторе удалим все и вставим шаблон.

При нажатии на PLAY в PRO редакторе, объект не будет смотреть в камеру. Почему?

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

Экспорт сцены в MyWebAR

Перед экспортом сцены с PRO редактора, необходимо удалить весь свет на сцене.

Далее нужно нажать на File → Publish.

Скачается архив. Этот архив нужно распаковать. В нем нужен файл app.json.

Этот файл нужно переименовать на любое другое название (это очень важно, если вы пропустите этот момент, сцена может загрузиться неправильно).

После этого перейдем в MyWebAR. В редакторе нажмем на JSON MAE.

Далее нужно загрузить json-файл.

Сделанная игра появилась на сцене.

Чтобы создать игру в расширенном редакторе, первое, что нужно сделать, это создать новый проект в , нажав на File → New.

продвинутом редакторе
Hidden Object