Wprowadzenie
Racer to internetowe eksperymenty mobilne w Chrome opracowane przez Active Theory. Do 5 znajomych może połączyć swoje telefony lub tablety, aby ścigać się na każdym ekranie. Mając do dyspozycji koncepcję, projekt i prototyp z Google Creative Lab oraz dźwięk z Plan8, przez 8 tygodni udoskonaliliśmy wersje, aż do premiery na konferencji I/O 2013. Gra jest już dostępna od kilku tygodni, więc mieliśmy okazję odpowiedzieć na kilka pytań od społeczności deweloperów dotyczących jej działania. Poniżej znajdziesz omówienie najważniejszych funkcji oraz odpowiedzi na najczęściej zadawane pytania.
Ścieżka
Jednym z wyzwań, przed którymi stanęliśmy, było stworzenie gry mobilnej w internecie, która działa dobrze na wielu różnych urządzeniach. Gracze musieli mieć możliwość tworzenia wyścigów na różnych telefonach i tabletach. Jeden z graczy może mieć Nexusa 4 i chcieć ścigać się z przyjacielem, który ma iPada. Musieliśmy znaleźć sposób na określenie wspólnego rozmiaru toru dla każdego wyścigu. Rozwiązanie musiało polegać na użyciu torów o różnych rozmiarach w zależności od specyfikacji poszczególnych urządzeń biorących udział w wyścigu.
Obliczanie wymiarów ścieżki
Gdy dołącza nowy gracz, informacje o jego urządzeniu są wysyłane na serwer i udostępniane innym graczom. Podczas tworzenia ścieżki te dane są używane do obliczenia jej wysokości i szerokości. Wysokość obliczamy, podając wysokość najmniejszego ekranu, a szerokość to łączna szerokość wszystkich ekranów. W przykładzie poniżej ścieżka ma więc szerokość 1152 pikseli i wysokość 519 pikseli.

this.getDimensions = function () {
var response = {};
response.width = 0;
response.height = _gamePlayers[0].scrn.h; // First screen height
response.screens = [];
for (var i = 0; i < _gamePlayers.length; i++) {
var player = _gamePlayers[i];
response.width += player.scrn.w;
if (player.scrn.h < response.height) {
// Find the smallest screen height
response.height = player.scrn.h;
}
response.screens.push(player.scrn);
}
return response;
}
Rysowanie ścieżki
Paper.js to platforma skryptowa do tworzenia grafik wektorowych typu open source, która działa na platformie HTML5 Canvas. Okazało się, że Paper.js to idealne narzędzie do tworzenia wektorowych kształtów ścieżek, więc wykorzystaliśmy jego możliwości do renderowania ścieżek SVG utworzonych w Adobe Illustrator na elemencie <canvas>
. Aby utworzyć ścieżkę, klasa TrackModel
dołącza kod SVG do DOM i zbiera informacje o pierwotnych wymiarach oraz pozycjonowaniu, które mają zostać przekazane do klasy TrackPathView
, która narysuje ścieżkę na płótnie.
paper.install(window);
_paper = new paper.PaperScope();
_paper.setup('track_canvas');
var svg = document.getElementById('track');
var layer = new _paper.Layer();
_path = layer.importSvg(svg).firstChild.firstChild;
_path.strokeColor = '#14a8df';
_path.strokeWidth = 2;
Po narysowaniu ścieżki każde urządzenie określa przesunięcie w osi X na podstawie swojej pozycji w kolejce urządzeń i odpowiednio ją ustawia.
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}

Animacje CSS
Paper.js wykorzystuje dużo procesora do rysowania torów, a ten proces zajmuje mniej lub więcej czasu na różnych urządzeniach. Aby to zrobić, musieliśmy umieścić ładujący się obraz w pętli, dopóki wszystkie urządzenia nie przetworzą utworu. Problem polegał na tym, że każda animacja oparta na JavaScript pomijała klatki z powodu wymagań procesora Paper.js. Wprowadzamy animacje CSS, które działają w ramach osobnego wątku interfejsu użytkownika. Dzięki temu możemy płynnie animować połysk w tekstu „BUILDING TRACK”.
.glow {
width: 290px;
height: 290px;
background: url('img/track-glow.png') 0 0 no-repeat;
background-size: 100%;
top: 0;
left: -290px;
z-index: 1;
-webkit-animation: wipe 1.3s linear 0s infinite;
}
@-webkit-keyframes wipe {
0% {
-webkit-transform: translate(-300px, 0);
}
25% {
-webkit-transform: translate(-300px, 0);
}
75% {
-webkit-transform: translate(920px, 0);
}
100% {
-webkit-transform: translate(920px, 0);
}
}
}
Sprite'y CSS
CSS przydał się też do efektów w grze. Urządzenia mobilne o ograniczonej mocy są zajęte animowaniem pojazdów poruszających się po torach. Aby dodać grze dodatkowego dynamizmu, użyliśmy sprite’ów, czyli gotowych animacji. W przypadku sprite’a CSS przejścia stosują animację krokową, która zmienia właściwości background-position
, tworząc eksplozję samochodu.
#sprite {
height: 100px;
width: 100px;
background: url('sprite.jpg') 0 0 no-repeat;
-webkit-animation: play-sprite 0.33s linear 0s steps(9) infinite;
}
@-webkit-keyframes play-sprite {
0% {
background-position: 0 0;
}
100% {
background-position: -900px 0;
}
}
Problem z tą metodą polega na tym, że można używać tylko arkuszy sprite’ów ułożonych w jednym rzędzie. Aby animacja mogła przewijać się w pętli przez wiele wierszy, musi być połączona za pomocą wielu deklaracji klatek kluczowych.
#sprite {
height: 100px;
width: 100px;
background: url('sprite.jpg') 0 0 no-repeat;
-webkit-animation-name: row1, row2, row3;
-webkit-animation-duration: 0.2s;
-webkit-animation-delay: 0s, 0.2s, 0.4s;
-webkit-animation-timing-function: steps(5), steps(5), steps(5);
-webkit-animation-fill-mode: forwards;
}
@-webkit-keyframes row1 {
0% {
background-position: 0 0;
}
100% {
background-position: -500px 0;
}
}
@-webkit-keyframes row2 {
0% {
background-position: 0 -100px;
}
100% {
background-position: -500px -100px;
}
}
@-webkit-keyframes row3 {
0% {
background-position: 0 -200px;
}
100% {
background-position: -500px -200px;
}
}
Renderowanie samochodów
Jak w przypadku każdej gry wyścigowej, wiedzieliśmy, że ważne jest, aby zapewnić użytkownikom uczucie przyspieszenia i sterowania. Stosowanie różnych wartości przyczepności było ważne dla zrównoważenia gry i zapewnienia rozrywki. Gdy gracz poznał już fizykę, mógł poczuć satysfakcję z osiągniętych wyników i został lepszym kierowcą.
Ponownie użyliśmy biblioteki Paper.js, która zawiera obszerny zestaw narzędzi do obliczeń. Użyliśmy niektórych jego metod do poruszania samochodem po ścieżce, jednocześnie dostosowując płynnie jego położenie i obrot w każdym ujęciu.
var trackOffset = _path.length - (_elapsed % _path.length);
var trackPoint = _path.getPointAt(trackOffset);
var trackAngle = _path.getTangentAt(trackOffset).angle;
// Apply the throttle
_velocity.length += _throttle;
if (!_throttle) {
// Slow down since the throttle is off
_velocity.length *= FRICTION;
}
if (_velocity.length > MAXVELOCITY) {
_velocity.length = MAXVELOCITY;
}
_velocity.angle = trackAngle;
trackOffset -= _velocity.length;
_elapsed += _velocity.length;
// Find if a lap has been completed
if (trackOffset < 0) {
while (trackOffset < 0) trackOffset += _path.length;
trackPoint = _path.getPointAt(trackOffset);
console.log('LAP COMPLETE!');
}
if (_velocity.length > 0.1) {
// Render the car if there is actually velocity
renderCar(trackPoint);
}
Podczas optymalizowania renderowania samochodów znaleźliśmy ciekawy punkt. W przypadku iOS najlepszą skuteczność uzyskaliśmy, stosując do samochodu transformację translate3d
:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
W Chrome na Androida najlepszą wydajność uzyskaliśmy, obliczając wartości macierzy i stosując jej transformację:
var rad = _rotation.rotation * (Math.PI * 2 / 360);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var a = parseFloat(cos).toFixed(8);
var b = parseFloat(sin).toFixed(8);
var c = parseFloat(-sin).toFixed(8);
var d = a;
_car.style.webkitTransform = 'matrix(' + a + ', ' + b + ', ' + c + ', ' + d + ', ' + _position.x + ', ' + _position.y + ')';
Synchronizacja urządzeń
Najważniejszym (i najtrudniejszym) etapem tworzenia gry było zapewnienie synchronizacji gry na różnych urządzeniach. Uznaliśmy, że użytkownicy mogą wybaczyć, jeśli samochód okazjonalnie przeskoczy kilka klatek z powodu wolnego połączenia, ale nie będzie to zbyt przyjemne, jeśli samochód będzie skakać i pojawiać się na kilku ekranach jednocześnie. Rozwiązanie tego problemu wymagało wielu prób i błędów, ale ostatecznie udało nam się znaleźć kilka sztuczek, które zadziałały.
Obliczanie opóźnienia
Punktem wyjścia do synchronizowania urządzeń jest poznanie czasu potrzebnego na odbieranie wiadomości z przekaźnika Compute Engine. Problem polega na tym, że zegary na poszczególnych urządzeniach nigdy nie będą całkowicie zsynchronizowane. Aby to obejść, musieliśmy znaleźć różnicę w czasie między urządzeniem a serwerem.
Aby znaleźć przesunięcie czasowe między urządzeniem a głównym serwerem, wysyłamy wiadomość z bieżącą sygnaturą czasową urządzenia. Serwer odpowie z pierwotną sygnaturą czasową oraz sygnaturą czasową serwera. Używamy odpowiedzi do obliczenia rzeczywistej różnicy w czasie.
var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;
Wykonanie tego raz nie wystarczy, ponieważ czas błądzenia do serwera nie zawsze jest symetryczny, co oznacza, że dotarcie odpowiedzi do serwera może zająć więcej czasu niż jej odesłanie z serwera. Aby temu zaradzić, odpytujemy serwer kilka razy i używamy mediany wyników. Dzięki temu różnica między urządzeniem a serwerem wynosi około 10 ms.
Przyspieszenie/spowolnienie
Gdy gracz 1 naciśnie lub puści ekran, na serwer zostanie wysłane zdarzenie przyspieszenia. Po otrzymaniu danych serwer dodaje bieżącą sygnaturę czasową i przekazuje te dane wszystkim innym graczom.
Gdy urządzenie otrzyma zdarzenie „przyspieszenie włączone” lub „przyspieszenie wyłączone”, możemy użyć przesunięcia serwera (obliczonego powyżej), aby sprawdzić, ile czasu zajęło otrzymanie tej wiadomości. Jest to przydatne, ponieważ gracz 1 może otrzymać wiadomość w 20 ms, a gracz 2 – w 50 ms. W efekcie samochód znalazłby się w 2 różnych miejscach, ponieważ urządzenie 1 zaczęłoby przyspieszać wcześniej.
Możemy wziąć czas potrzebny na otrzymanie zdarzenia i przekształcić go w klatki. Przy 60 fps każda klatka ma 16,67 ms – możemy więc dodać większą prędkość (przyspieszenie) lub tarcie (spowolnienie) samochodu, aby uwzględnić utracone klatki.
var frames = time / 16.67;
var onScreen = this.isOnScreen() && time < 75;
for (var i = 0; i < frames; i++) {
if (onScreen) {
_velocity.length += _throttle * Math.round(frames * .215);
} else {
_this.render();
}
}}
W przykładzie powyżej, jeśli gracz 1 ma samochód na ekranie i czas potrzebny na otrzymanie wiadomości jest krótszy niż 75 ms, system dostosuje prędkość samochodu, przyspieszając go, aby nadrobić różnicę. Jeśli urządzenie nie jest widoczne na ekranie lub wiadomość zajęła zbyt dużo czasu, funkcja renderowania uruchomi się i samochód przeskoczy do miejsca, w którym powinien się znaleźć.
Synchronizacja samochodów
Nawet po uwzględnieniu opóźnienia przyspieszenia samochód może się rozsynchronizować i pojawić na kilku ekranach jednocześnie, zwłaszcza podczas przełączania się z jednego urządzenia na drugie. Aby temu zapobiec, zdarzenia aktualizacji są wysyłane często, aby samochody były w tej samej pozycji na torze na wszystkich ekranach.
Zasada jest taka, że co 4 klatki, jeśli samochód jest widoczny na ekranie, urządzenie wysyła swoje wartości do wszystkich pozostałych urządzeń. Jeśli samochód jest niewidoczny, aplikacja aktualizuje wartości na podstawie otrzymanych wartości, a następnie przesuwa samochód do przodu w zależności od czasu, jaki upłynął od momentu otrzymania zdarzenia aktualizacji.
this.getValues = function () {
_values.p = _position.clone();
_values.r = _rotation;
_values.e = _elapsed;
_values.v = _velocity.length;
_values.pos = _this.position;
return _values;
}
this.setValues = function (val, time) {
_position.x = val.p.x;
_position.y = val.p.y;
_rotation = val.r;
_elapsed = val.e;
_velocity.length = val.v;
var frames = time / 16.67;
for (var i = 0; i < frames; i++) {
_this.render();
}
}
Podsumowanie
Gdy tylko usłyszeliśmy założenia Racera, wiedzieliśmy, że może to być bardzo wyjątkowy projekt. Szybko opracowaliśmy prototyp, który dał nam ogólne pojęcie, jak można ograniczyć opóźnienia i poprawić wydajność sieci. Był to wymagający projekt, który pochłaniał nas w póżne noce i podczas długich weekendów, ale gdy gra zaczęła nabierać kształtów, było to wspaniałe uczucie. Ostatecznie jesteśmy bardzo zadowoleni z otrzymanego wyniku. Koncepcja Google Creative Lab przesunęła granice technologii przeglądarek w ciekawy sposób, a my jako deweloperzy nie mogliśmy chcieć niczego więcej.