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:
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:
Ancak, HTML5'te, bit eşlenmiş bir imge kullanırız:
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:
Tile nesnesini dönüştürmek de benzer bir işlemdi. Flash'ta metin alanları ve vektör şekilleri kullandık:
HTML5'te, çalışma zamanında tek bir <canvas>
öğesinde üç birleşik resmi birleştiririz:
Ş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:
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ı:
Çö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:
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 &&
point.x < this.right &&
point.y > this.top &&
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:
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.
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.
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.