Форум

Как я делаю РПГ

02 августа 2015 05:17
Всем привет!! Сегодня расскажу про клиентскую часть, а именно про модули которые я реализовал и про то как они между собой взаимодействуют. Ну, поехали.

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

1. Сцена (или локация). 3D сцена окружающей среды
2. Персонаж. Управление человечком
3. Объект на локации. Загрузка всяких домиков и настройка их поведением и отображением
4. Карта. Собсна гугломап на которой отображаются локации, маршруты и т.п
5. Миссии. Модуль управляет получением/выполнением миссий. Казалось бы…
6.Технологии. Модуль управляет всякой научной деятельностью и собиранием велосипедов.
7. Профиль. То , что касается аккаунта игрока
8. Камера. Скорее всего Deprecated. Код этого модуля перекочует в другие модули

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

Главный модуль приложения APPLICATION
Этот модуль координирует работу всех остальных модулей. Его основная задача - подгрузка модулей и установка слушателей на события. Так же этот модуль рисует GUI (ну главный поток же вродь =))))
Подгрузка модулей динамическая. Сделано потому, что мне было лень писать много тэгов скрипт, потому что это не тру, и потому, что мы никак не можем узнать какой скрипт (а в нашем случае модуль) загрузится раньше, а какой позже и это грозит нам проблемами. Посему я написал функцию импорта

var imported = [];
var _import = function(moduleName, callback){
	if(!imported[moduleName]){
		imported[moduleName] = {cb:[callback], status:STATUS_LOADING};
		var s = document.createElement('script');
		s.src = '/' + moduleName + '.js?t=' + date.getTime();
		s.onload = function(){
			imported[moduleName].status = STATUS_READY;
			for(var i = 0; i < imported[moduleName].cb.length; i++)
				imported[moduleName].cb[i](require(moduleName));
		}
		document.body.appendChild(s);
	}
	else{
		if(imported[moduleName].status === STATUS_LOADING){
			imported[moduleName].cb.push(callback);
		}
		else{
			callback(require(moduleName));
		}
	}
}


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

Давайте рассмотрим простой пример. Пользователь кликнул по иконке локации на карте с целью перейти в неё. После загрузки окружающей среды нам необходимо показать пользователю всё, что лежит, ходит, спит и ест на этой локации. За отображение карты отвечает модуль MAP, за отрисовку окружающей среды модуль SCENE, за объекты на локации - LOCATION_OBJECT. Что бы всё это зарабтало, нам необходимо установить слушателя на клик по иконке и слушателя на загрузку среды. Сделать это мы можем примерно вот таким образом

SCENE.onLocationLoaded(function(id){
    LOCATION_OBJECT.loadOdjects(id);
});
MAP.onLocationClick(function(_location){
    SCENE.load(_location.id);
});


В коде всех модулей, установка слушателей выглядит одинаково

var _onSomeEvent;
var onSomeEvent = function(f){
    _onSomeEvent = f;
}


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

Второй неигровой модуль. SOCKET
Название говорит само за себя. Модуль отвечает за общение с сервером и чертовски прост. У него есть всего несколько методов:
1. bindEvents - на входе принимает массив объектов вида {eventName:'someEvent', callback:someFunction}; Аналогично главному модулю, назначает обработчиков на события сокета. Например: пользователю предложили заключить альянс и модуль PROFILE должен об этом как-то сигнализировать.
2. trigger - оболочка для socket.emit
Так же модуль обрабатывает зарезервированные события socket.io а-ля connect, disconnect и т.п.

"Игровые" модули
Подробно рассказывать о каждом модуле наверное стоит отдельно ибо каждый из них специфичен. Но кое-что общее у них всё же есть.
1. GUI. Каждый модуль представлен в приложении какой-нибудь кнопкой или целым виджетом. За генерацию своих кнопок отвечает модуль. За управление тоже. За рендер GUI отвечает модуль APPLICATION
2. Подписка на события. Каждый модуль сообщает модулю SOCKET на какие события и как он будет реагировать
3. В модулях инкапсулируется только то, что касается самого модуля.

Вот как-то так. Скоро ёще о чем-нибудь расскажу. Пока не придумал о чем… Рассказать есть много чего, но надо это как-то последовательно делать. А то я сам путаюсь .
Гале подарили мяч, Гале подарили торт, Галю поздравляют все - Галя сделала аборт
03 августа 2015 15:22
Этот модуль координирует работу всех остальных модулей. Его основная задача - подгрузка модулей и установка слушателей на события. Так же этот модуль рисует GUI (ну главный поток же вродь =))))
Мне кажется, вы его потом захотите вынести в отдельный модуль все-таки) Просто для удобства. Вроде бы, не самый тривиальный интерфейс должен быть

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

Второй неигровой модуль. SOCKET
Вот это очень интересно. Законченных сетевых приложений на b4w нет, и будет крайне интересно следить за тем, что выходит. Если дело удачно пойдет, я думаю, другим пользователям было бы интересно почитать урок на эту тему
03 августа 2015 16:05

Ответ на сообщение пользователя Евгений Родыгин
Мне кажется, вы его потом захотите вынести в отдельный модуль все-таки) Просто для удобства. Вроде бы, не самый тривиальный интерфейс должен быть
Вы про GUI? Не исключаю такой возможности, но пока всё это дело выглядит довольно просто А в коде это всё строится вот так

var setupUi = function(){
	var wrapper = document.createElement('div');
		wrapper.style.width = '270px';
		wrapper.style.height = '100%';
		wrapper.style.cssFloat = 'left';
		wrapper.style.overflowY = 'auto';
		
	wrapper.appendChild(PROFILE.getProfileWidget());
	wrapper.appendChild(PROFILE.getResourcesWidget());
	wrapper.appendChild(PROFILE.getBagWidget());
	wrapper.appendChild(TECHNOLOGIES.getMapControl());
	wrapper.appendChild(MISSIONS.getMapControl());
	wrapper.appendChild(TEAMS.getMapControl());
	wrapper.appendChild(HOME.getMapControl());
	wrapper.appendChild(SETTINGS.getMapControl());
	wrapper.appendChild(SCENE.getSoundControl());
	wrapper.appendChild(clock);
	MAP.getControlPanel(google.maps.ControlPosition.TOP_LEFT).appendChild(wrapper);
        //MAP.getControlPanel(SOME_PANEL).appendChild(SOME_HTML_ELEMENT);
}


Если дело удачно пойдет, я думаю, другим пользователям было бы интересно почитать урок на эту тему
Обязательно напишу в ближайшее время
Гале подарили мяч, Гале подарили торт, Галю поздравляют все - Галя сделала аборт
03 августа 2015 16:17

Ну и приследуем стиль функционального программирования. Т.е., модули выступают только как набор API и сами не выполняют никакой работы при загрузке.

Полностью согласен. Всё должно быть использовано только тогда, когда оно должно быть использовано.
var initLocationObject = function(){
	_import('EA_LOCATION_OBJECT', function(module){
		LOCATION_OBJECT = module;
		LOCATION_OBJECT.init();
		SOCKET.bindEvents(LOCATION_OBJECT.getSocketEvents());
		HOME.setBuildingOnClickListener(function(id){
			SOCKET.trigger('build', {buildingId:id, locationId:PROFILE.getCurrentLocation().id, x:0, y:0});
		});
		initScene();
	});
}
Гале подарили мяч, Гале подарили торт, Галю поздравляют все - Галя сделала аборт
13 августа 2015 04:40
Привет-привет! Сегодня речь пойдет о всяких машинках, домиках и прочей ерунде, загружаемой на локацию

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

Сначала о моделях
Модель какого-либо объекта (а в данном конкретном случае я говорю про модель ЗРК), состоит из каких-то функциональных частей. То есть мой ЗРК это не один меш, а траки отдельно, колёса отдельно, ракеты отдельно и тп. Такой подход одновременно и необходим и очень полезен. Во-первых, при желании мы можем стрельнуть в наш ЗРК из гранатомёта например и заставить его развалиться на эти самые составные части. Во-вторых, каждая из этих составных частей будет являться самостоятельным объектом, что позволит нам раскидать эти части по локациям и заставить игрока их искать чтоб потом из них собрать единое что-то. Это применимо как ко всякого рода машинкам, так и к постройкам, оружию и устройствам: например чтоб построить колодец нам нужно три бревна, цепь и ведро.

Настраиваем необходимую анимацию, физику и экспорт.

После экспорта, нам надо эту модель загрузить в игру. Казалось бы возьми просто и положи её в нужную папку… Ан нет… Дело в том, что нам в последствии нужно будет программно назначить ограничители для всех частей объекта. (Простите, если все настройки constraints из blendera нормально понимаются движком. честно не пробовал). Для этого я написал скрипт, которому скармливается результат экспорта из блендера, а он на выходе выдает нам интерфейс, в котором мы можем указать кто чей родитель и тип ограничителя: на данный момент stiff или semi-stiff (скрин ниже). Так же этот скрипт разберёт объект на детальки и создаст для них в БД экземпляры


Теперь к логике
Задача, не много ни мало, в следующем:
1. после загрузки окружающей среды, загрузить на сцену все объекты, находящиеся на локации.
2. собрать объекты
3. расположить объекты в нужных местах
4. повесить на эти объекты физику
5. научить объекты взаимодействовать с нашим персонажем
6. запустить анимацию
.
Загрузка объектов
После того, как мы вытащили из БД список всех объектов локации, нам надо эти объекты загрузить на сцену. Но перед загрузкой надо этот список немного отсортировать. Дело в том, что на локации может быть 2 и более одинаковых объектов, а смысла в том, чтобы дважды спрашивать у сервера одни и те же данные я не вижу. По-этому объекты мы будем тиражировать обычным objects.copy()

Сборка объектов
Так как мы определились, что все объекты состоят из деталек, каким-то образом нам надо их между собой связать чтоб потом адекватно перемещать\вращать. Для этого в БД для каждой детальки (кроме основной "корневой") указывается имя родительского меша и тип отношения между родителем наследником. Тип отношения задаётся флагом canRotate
var parentName = instance.parts[i].parts.data.parentMeshName + '_' + instance.locationObject.id;
var targetName = instance.parts[i].parts.data.meshName + '_' + instance.locationObject.id;
var parent = scene.get_object_by_name(parentName, sceneId);
var target = scene.get_object_by_name(targetName, sceneId);
if(instance.parts[i].parts.data.canRotate)
	constraints.append_semi_stiff(target, parent, getObjectTranslationOffset(target, parent));
else
	constraints.append_stiff(target, parent, getObjectTranslationOffset(target, parent));


Функция getObjectTranslationOffset нужна для того, чтоб определить смещение origin дочерней детальки относительно origin родительской. Это необходимо, потому что мы не можем расположить origin всех деталей в точке начала координат ибо правой двери нужно вращаться вокруг одной точки, левой двери вокруг другой точки, а крышке багажника вокруг сааааавсем другой.

Выглядит она так (знаю, что кривая… самому не нравится, но пока ничо лучше я не придумал)
var getObjectTranslationOffset = function(target, parent){
	var rt = transform.get_translation(target);
	var pt = transform.get_translation(parent);
	var ox = Math.abs(rt[0] - pt[0]);
	var oy = Math.abs(rt[1] - pt[1]);
	var oz = Math.abs(rt[2] - pt[2]);

	if(rt[0] < pt[0]) ox *= -1
	if(rt[1] < pt[1]) oy *= -1;
	if(rt[2] < pt[2]) ox *= -1;
	
	var offset = new Float32Array([ox,oy,oz]);
	return offset;
}


Перебрав в цикле все детальки, мы можем найти "корневую" детальку (например кузов). У этой детальки свойство parentMeshName будет NULL

Расположение объектов на локации
Так как все детальки у нас теперь будут следовать за корневой, нам достаточно указать только её координаты, которые мы вытаскиваем из БД. Так же из БД мы вытаскиваем угол поворота. По большей части нас волнует поворот только вокруг вертикальной оси, так как все остальные крены должны обуславливаться физикой и действием гравитации (не всегда конечно, но за большим исключением). Для поворота я использую немножко модифицированную функцию, приведённую для меня Романом Семенцовым, за что ему еще раз огромное спасибо.
var X = 0;
var Y = 1;
var Z = 2;
var rotateObject = function(object, axis, angle){
	var angle = Math.PI / (180 / angle);
	var oldRotaton = transform.get_rotation(object)
	var newRotation = new Float32Array(3);

	newRotation[axis] = angle;

	var newRotationQuat = util.euler_to_quat(newRotation);

	quat.multiply(oldRotaton, newRotationQuat, oldRotaton);
	transform.set_rotation(object, oldRotaton[0], oldRotaton[1], oldRotaton[2], oldRotaton[3]);
}
//Вызов rotateObject(obj, Y, 45);

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

Физика
С физикой всё хорошо, до тех пор пока она Static. Но как только, я делаю тип физики динамическим, детальки начинают расползаться в разные стороны. Видать реагируют на соударение между собой. И на ограничители им пофик. Пока не смог это победить и подобрать (именно подобрать, ибо я вааашпе не понимаю как это работает ) нужную комбинацию галочек в блендере. Как подберу обязательно расскажу как я это сделал.

Интерактив
Тут нас интересует два момента: когда персонаж игрока близко подошел к объекту и когда мы кликнули по объекту.
Первое соответственно через collision_sensor, второе через scene.pick_object(). При обоих этих событиях мы выводим список доступных с объектом действий в виде кнопочек
[{socketEventName:'someEvent', title:'Действие 1'}, ...., {socketEventName:'someEvent', title:'Действие N'}]

и показываем информацию об объекте а-ля иконка, название, описание…

Анимация
Ну скучно ведь, когда машинки стоят как на фотографии… Значит надо, чтоб они крутились по-всякому. Для этого в блендере для нужных деталек делаем объектную анимацию и называем её как {meshName}_idleAction и запускаем её программно. Так же, мы хотим, чтобы когда мы кликнули по крышке багажника, у нас (кроме отображения списка действий и информации) эта крышка открылась. Для этого создаём объектную анимацию {meshName}_selectAction. С этим проблем казалось бы нет… Но увы я не нашел способа программно получить список действий, которые повешены на объект. animation.get_anim_names() возвращает список всех экшнов и ни слова не говорит о том, кому какой экшн принадлежит. Закрадывается сомнение, что так и должно быть ибо экшну судя по всему пофик у какого объекта менять значения location/rotation/scale в соответствии со значениями в keyframe. (И это грустно ). Так же я не знаю как в блендере задать список экшнов для конкретного меша. Пока думаю что с этим делать…

Итог
Это, конечно, далеко не всё, что придётся пережить объектам локации. Все объекты разные и ведут себя по-разному, и уложить это всё в один пост было бы кощунством. Так что о специфике работы с разными типами объектов буду рассказывать в следующих постах. А пока, всем кавабанга
Гале подарили мяч, Гале подарили торт, Галю поздравляют все - Галя сделала аборт
13 августа 2015 08:34
Интересный проект. Я как то хотел сделать свою бродилку по гугл картам, используя данные из панорам улиц. А вдохновила меня вот эта демка. Когда машина гуглов ездит по городу, она снимает не только фото, но и простенькую геометрию.
Не стой, где попало… Попадет еще раз.
http://naviris.ru/
 
Пожалуйста, зарегистрируйтесь или войдите под своей учетной записью , чтобы оставлять сообщения.