Örnek Olay - Wordico'yu Flash'tan HTML5'e dönüştürme

Adam Mark
Adam Mark
Adrian Gould
Adrian Gould

Giriş

Wordico adlı bulmaca oyunumuzu Flash'tan HTML5'e dönüştürdüğümüzde ilk işimiz, tarayıcıda zengin bir kullanıcı deneyimi oluşturma hakkında bildiğimiz her şeyi unutmaktır. Flash, vektör çizimden poligon isabet algılamaya ve XML ayrıştırmaya kadar uygulama geliştirmenin tüm yönleri için tek ve kapsamlı bir API sunarken HTML5, farklı tarayıcı desteğine sahip bir dizi spesifikasyon sunuyordu. Ayrıca, belgeye özel bir dil olan HTML ve kutu odaklı bir dil olan CSS'nin oyun oluşturmak için uygun olup olmadığını da merak ettik. Oyun, Flash'ta olduğu gibi tarayıcılarda tekdüze bir şekilde gösterilir mi? Ayrıca, aynı şekilde güzel görünür ve çalışır mı? Wordico için yanıt evet oldu.

Vektörünüz nedir Victor?

Wordico'nun orijinal sürümünü yalnızca vektör grafikleri (çizgiler, eğriler, dolgular ve degradeler) kullanarak geliştirdik. Sonuç olarak hem son derece kompakt hem de sonsuz ölçeklenebilir bir çözüm elde edildi:

Wordico Wireframe
Flash'ta her görüntüleme nesnesi vektör şekillerinden oluşuyordu.

Ayrıca, birden fazla duruma sahip nesneler oluşturmak için Flash zaman çizelgesinden de yararlandık. Örneğin, Space nesnesi için dokuz adlandırılmış ana kare kullandık:

Flash'ta üç harfli bir boşluk.
Flash'ta üç harfli bir boşluk.

Ancak HTML5'te bit eşlemeli bir sprite kullanırız:

Dokuz alanın tümünü gösteren bir PNG sprite.
Dokuz alanın tümünü gösteren bir PNG sprite.

15x15 oyun tahtasını tek tek alanlardan oluşturmak için her alanın farklı bir karakterle (ör. üçlü harf için "t" ve üçlü kelime için "T") temsil edildiği 225 karakterlik bir dize gösterimi üzerinde iterasyon yaparız. Bu işlem Flash'ta oldukça basitti. Boşlukları damgalayarak bir ızgara halinde düzenledik:

var spaces:Array = new Array();

for (var i:int = 0; i < 225; i++) {
  var space:Space = new Space(i, layout.charAt(i));
  ...
  spaces.push(addChild(space));
}

LayoutUtil.grid(spaces, 15);

HTML5'te bu durum biraz daha karmaşıktır. Oyun tahtasını tek seferde bir kare boyamak için bitmap çizim yüzeyi olan <canvas> öğesini kullanırız. İlk adım, resim sprite'ını yüklemektir. Yüklendikten sonra, düzen notasyonunda iterasyon yaparak her iterasyonda resmin farklı bir bölümünü çizeriz:

var x = 0;  // x coordinate
var y = 0;  // y coordinate
var w = 35; // width and height of a space

for (var i = 0; i < 225; i++) {
  if (i && i % 15 == 0) {
    x = 0;
    y += w;
  }

  var imageX = "_dDFtTqQxm".indexOf(layout.charAt(i)) * 70;

  canvas.drawImage("spaces.png", imageX, 0, 70, 70, x, y, w, w);

  x += w;
}

Web tarayıcısında gösterilen sonuç aşağıdadır. Kanvasın kendisinin CSS gölgesi olduğunu unutmayın:

HTML5&#39;te oyun tahtası tek bir kanvas öğesidir.
HTML5'te oyun tahtası tek bir kanvas öğesidir.

Kart öğesini dönüştürmek de benzer bir işlemdi. Flash'ta metin alanları ve vektör şekilleri kullandık:

Flash karosu, metin alanlarının ve vektör şekillerinin bir kombinasyonuydu.
Flash karosu, metin alanlarının ve vektör şekillerinin bir kombinasyonuydu.

HTML5'te, çalışma zamanında üç resim sprite'ini tek bir <canvas> öğesinde birleştiririz:

HTML karosu üç resimden oluşur.
HTML karosu üç resimden oluşur.

Artık 100 kanvas (her karo için bir tane) ve oyun tahtası için bir kanvasımız var. "H" karosunun işaretlemesi aşağıda verilmiştir:

<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>

İlgili CSS şu şekildedir:

.tile {
  width: 35px;
  height: 35px;
  position: absolute;
  cursor: pointer;
  z-index: 1000;
}

.tile-drag {
  -moz-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
  -webkit-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
  -moz-transform: scale(1.10);
  -webkit-transform: scale(1.10);
  -webkit-box-reflect: 0px;
  opacity: 0.85;
}

.tile-locked {
  cursor: default;
}

.tile-racked {
  -webkit-box-reflect: below 0px -webkit-gradient(linear, 0% 0%, 0% 100%,  
    from(transparent), color-stop(0.70, transparent), to(white));
}

Kart sürüklenirken (gölge, opaklık ve ölçeklendirme) ve raftayken (yansıma) CSS3 efektleri uygularız:

Sürüklenen kart biraz daha büyük, biraz daha şeffaftır ve gölgeye sahiptir.
Sürüklenen kart biraz daha büyük, biraz daha şeffaftır ve gölgeye sahiptir.

Rastır resimlerin kullanılmasının bazı bariz avantajları vardır. İlk olarak, sonuç piksel hassasiyetindedir. İkinci olarak, bu resimler tarayıcı tarafından önbelleğe alınabilir. Üçüncü olarak, biraz daha çalışmayla resimleri değiştirerek yeni karo tasarımları (ör. metal karo) oluşturabiliriz. Bu tasarım çalışması, Flash yerine Photoshop'ta yapılabilir.

Dezavantajı nedir? Resim kullanarak metin alanlarına programatik erişimden vazgeçeriz. Flash'ta, türün rengini veya diğer özelliklerini değiştirmek basit bir işlemdi. HTML5'te ise bu özellikler resimlere yerleştirilmiştir. (HTML metnini denedik ancak çok fazla ek işaretleme ve CSS gerektiriyordu. Kanvas metnini de denedik ancak sonuçlar tarayıcılar arasında tutarlı değildi.)

Bulanık mantık

Tarayıcı penceresini her boyutta tam olarak kullanmak ve kaydırma yapmaktan kaçınmak istedik. Oyunun tamamı vektörlerle çizildiği ve doğruluğu kaybetmeden yukarı veya aşağı ölçeklendirilebildiği için bu işlem Flash'ta nispeten basitti. Ancak HTML'de bu işlem daha zordu. CSS ölçeklendirmesini kullanmaya çalıştık ancak bulanık bir kanvas elde ettik:

CSS ölçeklendirme (sol) ve yeniden çizme (sağ).
CSS ölçeklendirme (solda) ve yeniden çizme (sağda).

Çözümümüz, kullanıcı tarayıcıyı her yeniden boyutlandırdığında oyun tahtasını, rafı ve karoları yeniden çizmektir:

window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);

...
rack.setConstraints(rackWidth, rackHeight);

...
tileManager.resizeTiles(tileSize);
});

Sonuç olarak, her ekran boyutunda net resimler ve hoş düzenler elde ederiz:

Oyun tahtası dikey alanı doldurur; diğer sayfa öğeleri onun etrafında akar.
Oyun tahtası dikey alanı doldurur; diğer sayfa öğeleri etrafında akar.

Doğrudan konuya girin

Her karo kesinlikle konumlandırıldığı ve oyun tahtası ile rafla tam olarak hizalanması gerektiği için güvenilir bir konumlandırma sistemine ihtiyacımız var. Öğelerin genel alandaki (HTML sayfası) konumunu yönetmeye yardımcı olmak için Bounds ve Point adlı iki işlev kullanırız. Bounds, sayfadaki dikdörtgen bir alanı tanımlarken Point,sayfanın sol üst köşesine (0,0) göre bir x,y koordinatını (kayıt noktası olarak da bilinir) tanımlar.

Bounds ile iki dikdörtgen öğenin kesişim noktasını (ör. bir karo rafın üzerinden geçtiğinde) veya dikdörtgen bir alanın (ör. çift harfli boşluk) rastgele bir nokta (ör. karonun orta noktası) içerip içermediğini algılayabiliriz. Sınırların uygulanması aşağıda açıklanmıştır:

// bounds.js
function Bounds(element) {
var x = element.offsetLeft;
var y = element.offsetTop;
var w = element.offsetWidth;
var h = element.offsetHeight;

this.left = x;
this.right = x + w;
this.top = y;
this.bottom = y + h;
this.width = w;
this.height = h;
this.x = x;
this.y = y;
this.midx = x + (w / 2);
this.midy = y + (h / 2);
this.topleft = new Point(x, y);
this.topright = new Point(x + w, y);
this.bottomleft = new Point(x, y + h);
this.bottomright = new Point(x + w, y + h);
this.middle = new Point(x + (w / 2), y + (h / 2));
}

Bounds.prototype.contains = function (point) {
return point.x > this.left &amp;&amp;
point.x < this.right &amp;&amp;
point.y > this.top &amp;&amp;
point.y < this.bottom;
}

Bounds.prototype.intersects = function (bounds) {
return this.contains(bounds.topleft) ||
this.contains(bounds.topright) ||
this.contains(bounds.bottomleft) ||
this.contains(bounds.bottomright) ||
bounds.contains(this.topleft) ||
bounds.contains(this.topright) ||
bounds.contains(this.bottomleft) ||
bounds.contains(this.bottomright);
}

Bounds.prototype.toString = function () {
return [this.x, this.y, this.width, this.height].join(",");
}

Sayfadaki herhangi bir öğenin veya fare etkinliğinin mutlak koordinatını (sol üst köşe) belirlemek için Point değerini kullanırız. Point, animasyon efektleri oluşturmak için gerekli olan mesafe ve yönü hesaplama yöntemlerini de içerir. Point'ün uygulanması aşağıda açıklanmıştır:

// point.js

function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.distance = function (point) {
var a = point.x - this.x;
var b = point.y - this.y;

return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}

Point.prototype.distanceX = function (point) {
return Math.abs(this.x - point.x);
}

Point.prototype.distanceY = function (point) {
return Math.abs(this.y - point.y);
}

Point.prototype.interpolate = function (point, pct) {
var x = this.x + ((point.x - this.x) * pct);
var y = this.y + ((point.y - this.y) * pct);

return new Point(x, y);
}

Point.prototype.offset = function (x, y) {
return new Point(this.x + x, this.y + y);
}

Point.prototype.vector = function (point) {
return new Point(point.x - this.x, point.y - this.y);
}

Point.prototype.toString = function () {
return this.x + "," + this.y;
}

// static
Point.fromElement = function (element) {
return new Point(element.offsetLeft, element.offsetTop);
}

// static
Point.fromEvent = function (evt) {
return new Point(evt.x || evt.clientX, evt.y || evt.clientY);
}

Bu işlevler, sürükle ve bırak ve animasyon özelliklerinin temelini oluşturur. Örneğin, bir karonun oyun tahtasındaki bir alanla çakışıp çakışmadığını belirlemek için Bounds.intersects()'ü, sürüklenen bir karonun yönünü belirlemek için Point.vector()'ü ve hareket geçişi veya yumuşatma efekti oluşturmak için Point.interpolate()'yi zamanlayıcıyla birlikte kullanırız.

Her şeyi akışına bırakan biri

Sabit boyutlu düzenlerin Flash'ta oluşturulması daha kolay olsa da değişken düzenlerin HTML ve CSS kutu modeliyle oluşturulması çok daha kolaydır. Değişken genişlik ve yüksekliğe sahip aşağıdaki ızgara görünümünü düşünün:

Bu düzenin sabit boyutları yoktur: Küçük resimler soldan sağa, yukarıdan aşağıya doğru akar.
Bu düzenin sabit boyutları yoktur: Küçük resimler soldan sağa, yukarıdan aşağıya doğru akar.

Sohbet panelini de kullanabilirsiniz. Flash sürümünde, fare hareketlerine yanıt vermek için birden fazla etkinlik işleyici, kaydırılabilir alan için bir maske, kaydırma konumunu hesaplamak için matematiksel işlemler ve bunları bir araya getirmek için çok sayıda başka kod gerekiyordu.

Flash&#39;taki sohbet paneli güzel ancak karmaşıktı.
Flash'taki sohbet paneli güzel ancak karmaşıktı.

Buna karşılık HTML sürümü, sabit yüksekliğe ve overflow mülkünün gizli olarak ayarlandığı bir <div>'ten ibarettir. Kaydırma işleminin bize maliyeti yoktur.

CSS kutusu modelinin işleyiş şekli.
CSS kutu modelinin işleyiş şekli.

Bu gibi durumlarda (sıradan düzen görevleri) HTML ve CSS, Flash'tan daha iyi performans gösterir.

Beni duyabiliyor musunuz?

<audio> etiketiyle ilgili sorun yaşıyorduk. Belirli tarayıcılarda kısa ses efektlerini tekrar tekrar çalamıyorduk. İki geçici çözüm denedik. Öncelikle, ses dosyalarını daha uzun hale getirmek için sessiz alanlarla doldurduk. Ardından, birden fazla ses kanalında dönüşümlü oynatma denemesi yaptık. Hiçbir teknik tamamen etkili veya zarif değildi.

Sonunda kendi Flash ses oynatıcımızı kullanmaya ve yedek olarak HTML5 ses kullanmaya karar verdik. Flash'taki temel kod şu şekildedir:

var sounds = new Array();

function playSound(path:String):void {
var sound:Sound = sounds[path];

if (sound == null) {
sound = new Sound();
sound.addEventListener(Event.COMPLETE, function (evt:Event) {
    sound.play();
});
sound.load(new URLRequest(path));
sounds[path] = sound;
}
else {
sound.play();
}
}

ExternalInterface.addCallback("playSound", playSound);

JavaScript'de, yerleştirilmiş Flash Player'ı algılamaya çalışırız. Bu işlem başarısız olursa her ses dosyası için bir <audio> düğümü oluştururuz:

function play(String soundId) {
var src = "/audio/" + soundId + ".mp3";

// Flash
try {
var swf = window["swfplayer"] || document["swfplayer"];
swf.playSound(src);
}
// or HTML5 audio
catch (e) {
var sound = document.getElementById(soundId);
if (sound == null || sound == undefined) {
    var sound = document.createElement("audio");
    sound.id = soundId;
    sound.src = src;
    document.body.appendChild(sound);
}
sound.play();
}
}

Bunun yalnızca MP3 dosyaları için geçerli olduğunu unutmayın. OGG'yi desteklemediğimiz için bu dosya türü için herhangi bir işlem yapamıyoruz. Sektörün yakın gelecekte tek bir biçime karar vermesini umuyoruz.

Anket konumu

Oyun durumunu yenilemek için HTML5'te Flash'ta kullandığımızla aynı tekniği kullanırız: İstemci, 10 saniyede bir sunucudan güncelleme ister. Oyun durumu son anketten bu yana değiştiyse istemci değişiklikleri alır ve işler. Aksi takdirde hiçbir şey olmaz. Bu geleneksel anket tekniği, çok zarif olmasa da kabul edilebilir. Ancak oyun geliştikçe ve kullanıcılar ağ üzerinden gerçek zamanlı etkileşim beklemeye başladıkça uzun süreli anket veya WebSockets geçmek istiyoruz. Özellikle WebSocket'ler, oyun deneyimini iyileştirmek için birçok fırsat sunar.

Ne harika bir araç!

Hem ön uç kullanıcı arayüzünü hem de arka uç kontrol mantığını (kimlik doğrulama, doğrulama, kalıcılık vb.) geliştirmek için Google Web Toolkit'i (GWT) kullandık. JavaScript'in kendisi Java kaynak kodundan derlenir. Örneğin, Point işlevi Point.java'ten uyarlanmıştır:

package com.wordico.client.view.layout;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.DomEvent;

public class Point {
public double x;
public double y;

public Point(double x, double y) {
this.x = x;
this.y = y;
}

public double distance(Point point) {
double a = point.x - this.x;
double b = point.y - this.y;

return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
...
}

Bazı kullanıcı arayüzü sınıflarının, sayfa öğelerinin sınıf üyelerine "bağlandığı" karşılık gelen şablon dosyaları vardır. Örneğin, ChatPanel.ui.xml, ChatPanel.java ile eşleşir:

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">

<ui:UiBinder
xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui"
xmlns:w="urn:import:com.wordico.client.view.widget">

<g:HTMLPanel>
<div class="palette">
<g:ScrollPanel ui:field="messagesScroll">
    <g:FlowPanel ui:field="messagesFlow"></g:FlowPanel>
</g:ScrollPanel>
<g:TextBox ui:field="chatInput"></g:TextBox>
</div>
</g:HTMLPanel>

</ui:UiBinder>

Ayrıntıların tamamı bu makalenin kapsamı dışındadır ancak bir sonraki HTML5 projeniz için GWT'yi incelemenizi öneririz.

Java'yı neden kullanmalısınız? Öncelikle, sıkı yazım denetimi için. Dinamik yazım, JavaScript'de faydalı olsa da (ör. bir dizinin farklı türde değerler tutabilmesi) büyük ve karmaşık projelerde baş ağrısı olabilir. İkincisi, yeniden düzenleme özellikleri için. Binlerce kod satırında bir JavaScript yöntem imzasını nasıl değiştireceğinizi düşünün. Bu işlem kolay değildir. Ancak iyi bir Java IDE ile bu işlem çok kolaydır. Son olarak, test amacıyla. Java sınıfları için birim testleri yazmak, "kaydet ve yenile"nin eski tekniklerinden daha iyidir.

Özet

Ses sorunları dışında HTML5, beklentilerimizin çok üzerinde bir performans gösterdi. Wordico, Flash'ta olduğu kadar iyi görünmekle kalmaz, aynı zamanda aynı derecede akıcı ve duyarlıdır. Bunu Canvas ve CSS3 olmadan yapamazdık. Bir sonraki hedefimiz: Wordico'yu mobil kullanıma uyarlamak.