Показать сообщение отдельно
Старый 03.10.2006, 19:02
Dendroid вне форума Посмотреть профиль Отправить личное сообщение для Dendroid Посетить домашнюю страницу Dendroid Найти все сообщения от Dendroid
  № 12  
Ответить с цитированием
Dendroid
 
Аватар для Dendroid

Регистрация: Dec 1999
Адрес: Магадан
Сообщений: 763
По теме, вот мой простой и надёжный способ синхронизации между двумя игроками на основе getTimer() - синхронизируем все события относительно количества пройденных миллисекунд одного из игроков (в дальнейшем "мастера"). AS 3, вполне рабочий код - вставить в первый фрейм и скомпилировать:

Код:
import flash.*;

var connection:NetConnection;
var game_so:SharedObject;

// количество циклов для синхронизации:
var so_cycles:int = 12;

NetConnection.defaultObjectEncoding = flash.net.ObjectEncoding.AMF0;
SharedObject.defaultObjectEncoding = flash.net.ObjectEncoding.AMF0;

// имена игроков берутся из передаваемых переменных login и adv, этот код для тестирования на локальной машине (чтобы имена были разные):
if (loaderInfo.parameters.login==null) {
	var mySo = SharedObject.getLocal("testname");
	if (mySo.data.name == "player1") {
		login = "player2"
		adv = "player1";
	}else{
		login = "player1"
		adv = "player2";
	}
	mySo.data.name = login;
	mySo.flush();
	delete mySo;
} else {
	login = loaderInfo.parameters.login;
	adv = loaderInfo.parameters.adv;
}

// определяем кто будет мастер (по чьему таймлайну будут синхронизироваться все события в игре (самый тупой способ):
var so_main:Boolean = login>adv;

// чтобы каждая пара игроков (имена уникальны на сервере) получала уникальный sharedoject в пользование:
var so_name:String = so_main?login+"__"+adv:adv+"__"+login;

// переменная, которая будет определять сдвиг времени для событий, чтобы привести их к таймлайну мастера (у мастера это значение останется нулём):
var timeShift:Number = 0;

var timeShiftStat:Array = new Array(so_cycles);

connection = new NetConnection();
connection.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
connection.connect("rtmp://[ваше приложение]");

function netStatusHandler(event:NetStatusEvent):void {
	switch (event.info.code) {
	case "NetConnection.Connect.Success":
		connectSO();
		break;
	case "NetConnection.Connect.Failed":
		trace("failed");
		break;
	}
}

function connectSO () {
	game_so = SharedObject.getRemote(so_name, connection.uri, false);
	if(game_so){
		game_so.addEventListener(SyncEvent.SYNC, soOnSync);
		// нужно, если игроки могут присоединяться по локальной сети (маленький пинг):
		game_so.fps = 1000;
		game_so.connect(connection);
	}
}


function soOnSync(event:SyncEvent):void {
	if (game_so.data.ts == null) {
		if (!so_main) {
			// начинает синхронизацию слейв, путём инициализации значения ts.n нулём:
			game_so.setProperty("ts",{n:0,t:getTimer()});
		}
	} else {
		if (game_so.data.ts.n<so_cycles) {
			// в массиве сохраняем время каждого события синхронизации:
			timeShiftStat[game_so.data.ts.n] = game_so.data.ts.t;
			// четные значения посылаются слейвом, нечётные мастером:
			if (game_so.data.ts.n%2 ^ so_main) {
				game_so.setProperty("ts",{n:game_so.data.ts.n+1,t:getTimer()});
			}
		} else {
			calculateTimeShift();
		}
	}
}

// подсчитаем сдвиг времени для слейва:
function calculateTimeShift () {
	var arr:Array=[];
	for (var i:int=1; i<so_cycles-2; i+=2) {
		arr.push((timeShiftStat[i+2]+timeShiftStat[i])/2-timeShiftStat[i+1]);
	}
	// для небольшой дисперсии вполне хватает такого тривиального выбора среднего (для более точного подсчёта можно вычислять равноудалённое, среднее арифметическое, и не забыть отбросить слишком большие и слишком маленькие значения):
	arr.sort();
	timeShift = arr[Math.floor(arr.length/2)];
}
Как теперь работать с объектами? Этот код здесь не приведён, зависит от структуры игры. Это очень просто: пусть игрок слэйв хочет запустить новый объект (мяч, ракету), или провзаимодействовать уже с существующим, ему достаточно передать момент создания этого объекта, его положение и скорость, например в новой (или уже существующей - если объект уже был) переменной того же шаредобжекта: "game_so.setProperty("ball1",{x:10,y:10,vx:1,vy:0,t:getTimer()+timeShift});" Когда мастер получит эти значения, он создаст (переместит уже существующий объект) в нужное место (не забываем, что время создания уже в прошлом, поэтому объект надо переместить сразу в положение {x,y} + {vx,vy}*(getTimer()-t). Далее объект обсчитывается как движущийся независимо (не требует новых передач данных по сети), при этом он может взаимодействовать с другими объектами, движущимися независимо - это тоже не требует новых данных и будет работать синхронно для всех игроков, пока не провзаимодействует с одним из объектов, управляемых игроком (тогда переменная ball1 должна быть изменена или удалена в случае уничтожения объекта).

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

Метод удобен для игр, где есть объекты, которые движутся по заданным тракториям, пока на них не подействует один из игроков, когда не надо постоянно транслировать картину игры друг другу (аркады очень многие, мноопользовательские рпг, разбитые на локейшоны). Достаточно передавать изменения и новую траекторию объекта. Эти объекты синхронизируются идеально (у меня получается разница порядка 10миллисекунд при пингах в районе 300-500, это значит, например, что поставив два компьютера рядом, и законнектив их через сервер на другом полушарии, вы не сможите увидеть разницу в движении независимых объектов).

Проблема, естественно, остаётся с объектами, контролируемыми игроками, при пинге порядка полусекунды, если мы будем просто транслировать текущие координаты - будет заметно дёрганье, но тут есть хитрость: для этих объектов можно использовать различные алгоритмы компенсации, в самом простом случае передаём кроме положения ещё и скорость объекта. Пока не поступили новые данные, движим объект равномерно и прямолинейно. Как только поступили новые координаты удалённого игрока - не перемещаем объект сразу в место, где он должен быть, а делаем это плавно, в течении, скажем 300 миллисекунд - это существенно сгладит рывки объекта. В идеале оперировать надо кроме положения и скорости ещё и ускорением, чтобы вторая производная тоже была гладкой - тогда рывков вообще не будет заметно и будет полное впечатление, что синхронизация постоянная, как на локальной машине. Да, при таком "размытом" определении положения объекта игрока не сделать какое-нибудь полноконтактное взаимодействие (сложный фехтовальный бой или что-то вроде мортал комбата - но это проблема не только во флэше) - просто не будет хватать передаваемых данных, интерактивности взаимодействия.

Ещё приведённый выше кода и подход в целом хорош тем, что не требует никакой серверной части, скажу больше: он даже не требует своего сервера, ведь нужно только права открывать свои шареные обжекты, достаточно присоединиться к любому rtmp приложению, открытому в веб (к примерам на Адобе или на сервере примеров red5 Но так делать нехорошо, разве что для теста...). Вся обработка событий вынесена на сторону клиента, поэтому скорость симметрична относительно разницы пинга игроков, фактически работаем с суммой пингов, это обеспечивает один из условий fairplay (игрок с маленьким пингом не имеет преимущества перед игроком с большим пингом).

Вот ещё картинка, поясняющая, как мы считаем timeShift (в данном случае первым было запущено приложение слэйва (на рисунке справа), он установил значение t0 и ждёт, кода мастер (слева на временной шкале) продолжит синхронизацию. timeShift в этом примере получится отрицательным и равным (3200+2000)/2-9150= -6550 (т.е. по усреднению выборки считаем, что событие t4 у слэйва произошло в точности посередине событий t3 и t5 у мастера). Черные точки на линиях показывают, в какой момент событие может доходить до сервера и почему это не влияет на вычисления (из симметрии).
Изображения
 
__________________
Верить никому нельзя. Мне - можно. :)