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

Adam Mark
Adam Mark
Adrian Gould
Adrian Gould

Giriş

Wordico bulmacamızı Flash'tan HTML5'e dönüştürdüğümüzde ilk görevimiz, tarayıcıda zengin bir kullanıcı deneyimi oluşturma hakkında bildiğimiz her şeyi öğrenmek oldu. Flash, uygulama geliştirmenin tüm yönleri için (vektör çiziminden çokgen isabet algılamaya ve XML ayrıştırmaya kadar) tek ve kapsamlı bir API sunmuştu. HTML5 ise değişen tarayıcı desteğine sahip bir dizi spesifikasyon sunuyordu. Dokümana özgü bir dil olan HTML'nin ve kutu merkezli bir dil olan CSS'nin oyun geliştirmek için uygun olup olmadığını da merak ettik. Oyun, Flash'ta olduğu gibi tarayıcılar arasında eşit bir şekilde görüntülenir mi, aynı şekilde iyi görünür mü ve davranırdı? Wordico'ya cevap evet oldu.

Vektörün hangisi Victor?

Wordico'nun orijinal sürümünü yalnızca vektör grafikleri kullanarak geliştirdik: çizgiler, eğriler, dolgular ve gradyanlar. Sonuç olarak hem son derece kompakttı hem de sınırsız ölçeklenebilirliğe sahipti:

Wordico Tel Çerçeve
Flash'ta tüm görüntüleme nesneleri vektör şekillerinden oluşuyordu.

Birden çok durumu olan nesneler oluşturmak için de Flash zaman çizelgesinden yararlandık. Örneğin, Space nesnesi için dokuz adlandırılmış animasyon karesi kullandık:

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

Ancak, HTML5'te, bit eşlenmiş bir imge kullanırız:

Dokuz boşluğun tümünü gösteren bir PNG imgesi.
Dokuz boşluğun tümünü gösteren bir PNG imgesi.

15x15 boyutunda oyun tahtasını tek tek alanlardan oluşturmak için, her boşluğun farklı bir karakterle (üç harf için "t" ve üç kelime için "T" gibi) temsil edildiği 225 karakterlik bir dize gösterimi üzerinden yineleme yaparız. Bu, Flash'ta basit bir işlemdi. Boşlukları damgalayıp bir ızgarada 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 biraz daha karmaşıktır. Oyun tahtasını her defasında bir kare boyamak için bit eşlem çizim yüzeyi olan <canvas> öğesini kullanırız. İlk adım, resim imgesini yüklemektir. Yüklendikten sonra, her iterasyonda resmin farklı bir bölümünü çizerek düzen gösterimini yineliyoruz:

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 sonuç şöyle. Tuvalin kendisinde bir CSS gölgesi olduğuna dikkat edin:

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

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

Flash parçası, metin alanları ile vektör şekillerinin birleşiminden oluşuyordu
Flash parçası, metin alanları ile vektör şekillerinin bir kombinasyonundan oluşuyordu.

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

HTML parçası, üç resimden oluşur.
HTML kutusu üç resimden oluşur.

Şimdi 100 kanvas (her karo için bir tane) ve oyun tahtası için bir kanvas var. Burada, "H" blokuna ilişkin işaretleme gösterilmektedir:

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

İlgili CSS:

.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));
}

CSS3 efektlerini, karo sürüklenirken (gölge, opaklık ve ölçekleme) ve karo rafta otururken (yansıma) uygularız:

Sürüklenen karo biraz daha büyüktür, biraz şeffaftır ve gölgesi vardır.
Sürüklenen karo biraz daha büyüktür, biraz şeffaf ve üzerinde gölge vardır.

Kafes resimleri kullanmanı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 ekstra çalışma yaparak resimleri değiştirerek metal karo gibi yeni karo tasarımlarını oluşturabiliyoruz ve bu tasarım işi Flash yerine Photoshop'ta gerçekleştirilebiliyor.

Dezavantajı ne mi? Resimleri kullanarak metin alanlarına programlı erişim sağlarız. Flash'ta, türün rengini veya diğer özelliklerini değiştirmek basit bir işlemdi. HTML5'te ise bu özellikler resimlerin kendisine eklenir. (HTML metnini denedik ancak fazladan biçimlendirme ve CSS gerekiyordu. Tuval metnini de denedik, ancak sonuçlar tarayıcılar arasında tutarsızdı.)

Bulanık mantık

Tarayıcı penceresinden her boyutta tam anlamıyla yararlanmak ve sayfayı kaydırmaktan kaçınmak istedik. Bu, Flash'ta nispeten basit bir işlemdi, çünkü tüm oyun vektörlerde çizildi ve resim kalitesinden ödün vermeden ölçeği artırılıp azaltılabiliyordu. Ancak HTML'de daha karışıktı. CSS ölçeklendirmesini kullanmayı denedik ancak tuval bulanık çıktı:

CSS ölçeklendirmesi (sol) ve yeniden çizim (sağ).
CSS ölçeklendirmesi (sol) ve yeniden çizim (sağ) karşılaştırması.

Çözümümüz, kullanıcı tarayıcıyı 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);
});

Her ekran boyutunda net görseller ve hoş düzenler elde ederiz:

Oyun tahtası dikey alanı kaplar ve etrafında diğer sayfa öğeleri akar.
Oyun tahtası dikey alanı kaplar ve diğer sayfa öğeleri dikey alanı kaplar.

Doğrudan konuya girin

Her kutu eksiksiz bir şekilde konumlandırıldığı ve oyun tahtası ile rafın tam olarak hizalanması gerektiği için güvenilir bir konumlandırma sistemine ihtiyacımız var. Öğelerin global alandaki (HTML sayfası) konumlarını yönetmeye yardımcı olması için iki işlev (Bounds ve Point) kullanırız. Bounds, sayfadaki dikdörtgen bir alanı tanımlarken Point, sayfanın sol üst köşesine göre bir x,y koordinatını (0,0) ifade eder. Bu koordinat, kayıt noktası olarak da bilinir.

Bounds ile iki dikdörtgen öğenin kesişimini (örneğin, bir karo rafın üzerinden geçtiğinde) veya dikdörtgen bir alanın (çift harfli boşluk gibi) rastgele bir nokta (ör. bir karonun merkez noktası) içerip içermediğini tespit edebiliriz. Sınırların uygulanması şöyledir:

// 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 kullanırız. Point, animasyon efektleri oluşturmak için gerekli olan mesafe ve yönü hesaplamaya yönelik yöntemler de içerir. Point kullanımı şöyledir:

// 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ükleyip bırakma ve animasyon özelliklerinin temelini oluşturur. Örneğin, bir karonun oyun tahtasındaki bir boşlukla çakışıp çakışmadığını belirlemek için Bounds.intersects(), sürüklenen karonun yönünü belirlemek için Point.vector() ve yarı yarıya hareket veya yumuşatma efekti oluşturmak için bir zamanlayıcıyla birlikte Point.interpolate() kullanırız.

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

Flash'ta sabit boyutlu düzenlerin oluşturulması daha kolayken, akış düzenlerinin HTML ve CSS kutusu modeliyle oluşturulması çok daha kolaydır. Değişken genişliği ve yüksekliğine sahip aşağıdaki tablo görünümünü ele alalım:

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 hareket eder.

Dilerseniz sohbet panelini de değerlendirebilirsiniz. Flash sürümü; fare işlemlerine yanıt vermek için birden çok etkinlik işleyici, kaydırılabilir alan için bir maske, kaydırma konumunun hesaplanması için matematik ve bunu bir araya getirecek başka birçok kod gerektiriyordu.

Flash&#39;taki sohbet paneli güzel ama karmaşıktı.
Flash'taki sohbet paneli oldukça karmaşıktı.

Karşılaştırıldığında, HTML sürümü yalnızca sabit yüksekliğe sahip bir <div> ve taşma özelliği gizli olarak ayarlanmış. Kaydırmanın bize maliyeti yok.

Çalışmakta olan CSS kutusu modeli.
Kullanımdaki CSS kutu modeli.

Bu gibi durumlarda (normal düzen görevleri), HTML ve CSS, Flash'ı gölgede bırakır.

Şimdi beni duyabiliyor musun?

<audio> etiketiyle uğraştık. Yalnızca belirli tarayıcılarda kısa ses efektlerini tekrar tekrar oynatamıyordu. Bunun için iki çözüm denedik. İlk olarak, daha uzun olmaları için ses dosyalarını durgun havayla doldurduk. Daha sonra birden fazla ses kanalında sırayla oynatmayı denedik. Tekniklerin ikisi de tamamen etkili veya zarif değildi.

En sonunda kendi Flash ses oynatıcımızı kullanmaya ve yedek olarak HTML5 sesi 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'te, yerleştirilmiş Flash oynatıcıyı tespit etmeye çalışırız. Bu 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 çalıştığını unutmayın; OGG'yi hiçbir zaman desteklemek için uğraşmadık. Sektörün yakın gelecekte tek bir biçimi tercih etmesini umuyoruz.

Anket konumu

Oyun durumunu yenilemek için HTML5'te Flash'ta kullandığımız tekniğin aynısını kullanırız: Her 10 saniyede bir, istemci sunucudan güncelleme ister. Son anketten sonra oyun durumu değiştiyse istemci, değişiklikleri alır ve uygular. Aksi takdirde hiçbir şey olmaz. Bu geleneksel anket tekniği, oldukça şık olmasa da kabul edilebilir bir yöntemdir. Ancak, oyun olgunlaştıkça ve kullanıcılar ağ üzerinden gerçek zamanlı etkileşim beklemeye başladıkça, uzun yoklama veya WebSockets'e geçmek istiyoruz. Özellikle WebSockets, oyunu geliştirmek için birçok fırsat sunar.

Muhteşem bir araç!

Hem kullanıcı arabirimi kullanıcı arayüzü hem de arka uç kontrol mantığı (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, Nokta işlevi Point.java işlevinden 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ında, sayfa öğelerinin sınıf üyelerine "bağlı" olduğu karşılık gelen şablon dosyaları bulunur. Örneğin, ChatPanel.ui.xml, ChatPanel.java değerine karşılık gelir:

<!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>

Tüm ayrıntılar bu makalenin kapsamı dışındadır, ancak bir sonraki HTML5 projeniz için GWT'ye göz atmanızı öneririz.

Java'yı neden kullanmalısınız? Birincisi, katı yazımla ilgili. Dinamik yazma, JavaScript'te yararlı olsa da (bir dizinin farklı türlerdeki değerleri tutabilmesi gibi), büyük ve karmaşık projelerde baş ağrısı olabilir. İkincisi, özellikleri yeniden düzenlemek. Binlerce kod satırında bulunan bir JavaScript yöntem imzasını nasıl değiştireceğinizi düşünün, ama bunu yapmak kolay olmayabilir! Ancak iyi bir Java IDE ile bu çok kolay olur. Son olarak, test amaçlıdır. Java sınıfları için birim testleri yazmak, köklü "kaydet ve yenile" tekniğini geride bırakıyor.

Özet

Sesle ilgili sorunlar dışında HTML5 beklentilerimizi fazlasıyla aştı. Wordico, Flash'ta olduğu kadar iyi görünmekle kalmıyor, aynı zamanda akıcı ve duyarlı tasarıma da sahip. Bunu Tuval ve CSS3 olmadan yapamazdık. Bir sonraki görevimiz: Wordico'yu mobil kullanım için uyarlama.