Чтобы объекты на сцене не были черными, нужно просто добавить два источника света. В данном случае 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. Благодаря им игрок понимает, какой предмет ему нужно найти.
После того, как сцена заполнилась игровыми объектами, можно приступать к написанию кода.
Сам код делится на определенные блоки:
Основная механика. Здесь будет находиться функционал самой игры.
Работа с звуком. Воспроизведение звуков реакции персонажа на действия игрока.
Работа с анимацией. Запуск анимации персонажа, который будет реагировать на действия игрока.
Функционал стрелки-подсказки. Появление стрелки над предметом, который нужно найти. Появление должно срабатывать, когда пользователь не совершал никаких действий в течение последних n-секунд.
Основная механика игры
Основная механика игры заключается в случайной активации определенных объектов (всего 5 из 20 созданных). Активные объекты кликабельны. При правильном клике объекты и детали меняют цвет.
Итак, первое, что нужно сделать, это создать скрипт, привязанный к сцене, нажав на кнопку New.
Далее необходимо перейти в редактор кода, нажав на кнопку Edit.
Теперь можно приступить непосредственно к написанию кода.
Первое, что нужно сделать, это создать две переменные, которые будут содержать уже созданные GroupOfObjects и GroupOfTemplates (строки 1-2).
Далее я создам цикл с двадцатью итерациями (количество предметов для поиска).
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.
Для начала я добавлю шаблон в самом начале кода игровой механики.
Нет смысла разбирать работу этого шаблона. Вместо этого ниже мы рассмотрим, как его использовать.
Первое, что необходимо сделать, это добавить ссылки на аудиофайлы, которые вы хотите воспроизвести. Ссылки должны быть добавлены внутри объекта, расположенного на строках 9-13.
Допустим, вы хотите добавить три аудиофайла. Первый будет воспроизводиться при неправильном ответе, второй — при правильном ответе, а третий — при прохождении игры.
Мы можем изменить имена ключей свойств. Названия изменятся следующим образом: sound1 → wrong, sound2 → right, sound3 → win.
Следующим шагом будет непосредственно запуск аудио. Для этого в данном шаблоне есть специальная функция 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):
let oneTimeFlag = false; — простой флаг, который будет менять свое значение.
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);
Часы, для работы с анимациями.
Главный персонаж.
Первая дорожка с анимацией у главного персонажа.
Вторая дорожка с анимацией у галвного персонажа.
Миксер, служащий для запуска анимаций.
Анимационный клип для первой анимации.
Анимационный клип для второй анимации.
Далее сделаем так, чтобы вторая анимация всегда останавливалась на последнем кадре (строка 8) и отключим зацикливание (строка 9).
Чтобы анимация заработала, необходимо добавить условие (строки 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, просто добавьте следующие строчки:
var isMae = typeof window.editor === 'object' && typeof window.editor.fromJSON === 'function'; — перед созданием переменных.
if (isMae) delta /= 1000; if(!inited) return; if(typeof(Helper) === 'undefined') event.delta /= 1000; — внутри функции update().
Полный код с учетом данных исправлений (строки 12 и 14-22).
Теперь анимация будет правильно работать как в расширенном редакторе, так и в самом MyWebAR.
Следующим шагом будет добавление анимации-реакции на победу в игре (когда все предметы найдены).
В конце подраздела было создано условие внутри функции update(), которое срабатывает после того, как игрок нашел все предметы на сцене. Внутри этого условия можно добавить код для запуска определенной анимации. Эта анимация запускается вместе со звуком победы (строки 36-39).
Полный код сцены, включая звук, механику и анимацию:
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.