מבוא
כשהעברנו את משחק הפאזל Wordico מ-Flash ל-HTML5, המשימה הראשונה שלנו הייתה לשכוח את כל מה שידענו על יצירת חוויית משתמש עשירה בדפדפן. ב-Flash יש ממשק API יחיד ומקיף לכל ההיבטים של פיתוח אפליקציות – החל מאיור וקטור ועד לזיהוי פגיעה בפוליגון ולניתוח XML. לעומת זאת, ב-HTML5 יש מקבץ של מפרטים עם תמיכה משתנה בדפדפנים. תהינו גם אם HTML, שפה ספציפית למסמכים, ו-CSS, שפה שמתמקדת בתיבות, מתאימות ליצירת משחק. האם המשחק יוצג בצורה אחידה בכל הדפדפנים, כמו ב-Flash, והאם הוא ייראה ויפעל בצורה נעימה כמו ב-Flash? התשובה לגבי Wordico הייתה כן.
מהו הווקטור שלך, ויקטור?
פיתחנו את הגרסה המקורית של Wordico באמצעות גרפיקה וקטורית בלבד: קווים, עקומות, מילוי וגוונים. התוצאה הייתה קומפקטית מאוד וניתנת להתאמה לעומס (scalable) ללא הגבלה:
בנוסף, השתמשנו בציר הזמן של Flash כדי ליצור אובייקטים עם כמה מצבים. לדוגמה, השתמשנו בתשעת מפתחות ראשיים עם שם לאובייקט Space
:
עם זאת, ב-HTML5 אנחנו משתמשים ב-sprite בפורמט bitmap:
כדי ליצור את לוח המשחק בגודל 15x15 ממרחב משותף בודד, אנחנו מבצעים איטרציה על סימון מחרוזת של 225 תווים, שבו כל מרחב משותף מיוצג על ידי תו שונה (למשל 't' עבור אות משולשת ו-'T' עבור מילה משולשת). זו הייתה פעולה פשוטה ב-Flash. פשוט יצקנו את המרחבים והסדרנו אותם ברשת:
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, התהליך קצת יותר מורכב. אנחנו משתמשים באלמנט <canvas>
, משטח ציור של קובץ בייטמאפ, כדי לצייר את לוח המשחק, ריבוע אחד בכל פעם. השלב הראשון הוא טעינת ה-sprite של התמונה. אחרי הטעינה, אנחנו עוברים על סימון הפריסה ומציירים חלק אחר של התמונה בכל חזרה:
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;
}
זו התוצאה בדפדפן האינטרנט. שימו לב שלבד הציור יש הטלת צללית ב-CSS:
המרת אובייקט המשבצת הייתה תהליך דומה. ב-Flash, השתמשנו בשדות טקסט ובצורות וקטוריות:
ב-HTML5, אנחנו משלבים שלושה ספרייטים של תמונות ברכיב <canvas>
יחיד בזמן הריצה:
עכשיו יש לנו 100 משטחי קנבס (אחד לכל משבצת) ועוד משטח קנבס ללוח המשחק. זהו הרכיב של אריח מסוג 'H':
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
זהו הקוד של 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 כשהמשבצת גוררת (צל, אטימות ושינוי קנה מידה) וכשהמשבצת נמצאת במדף (השתקפות):
לשימוש בתמונות רסטר יש כמה יתרונות ברורים. קודם כול, התוצאה מדויקת עד לפיקסל. שנית, התמונות האלה יכולות להישמר במטמון של הדפדפן. שלישית, עם קצת עבודה נוספת, אנחנו יכולים להחליף את התמונות כדי ליצור עיצובים חדשים של משבצות – כמו משבצת מתכת – ואפשר לבצע את עבודת העיצוב הזו ב-Photoshop במקום ב-Flash.
החיסרון? כשמשתמשים בתמונות, אנחנו מוותרים על גישה פרוגרמטית לשדות הטקסט. ב-Flash, שינוי הצבע או מאפיינים אחרים של הסוג היה פשוט. ב-HTML5, המאפיינים האלה מוטמעים בתמונות עצמן. (ניסינו טקסט HTML, אבל נדרשו הרבה תגי markup ו-CSS נוספים. ניסינו גם טקסט על קנבס, אבל התוצאות לא היו עקביות בין הדפדפנים).
לוגיקה פאזית
רצינו לנצל את חלון הדפדפן במלואו בכל גודל – ולהימנע מגלישה. זו הייתה פעולה פשוטה יחסית ב-Flash, כי כל המשחק צויר בווקטורים וניתן היה לשנות את הגודל שלו למעלה או למטה בלי לאבד את איכות התצוגה. אבל זה היה מורכב יותר ב-HTML. ניסינו להשתמש בשינוי קנה מידה ב-CSS, אבל התוצאה הייתה קנבס מטושטש:
הפתרון שלנו הוא לצייר מחדש את לוח המשחק, את המארז ואת המשבצות בכל פעם שהמשתמש משנה את גודל הדפדפן:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
התוצאה היא תמונות חדות ותצוגות מרהיבות בכל גודל מסך:
התמקדות בנושא
מכיוון שכל משבצת ממוקמת באופן מוחלט וצריך להתאים אותה במדויק ללוח ולמארז, אנחנו זקוקים למערכת מיקום מהימנה. אנחנו משתמשים בשתי פונקציות, Bounds
ו-Point
, כדי לנהל את המיקום של רכיבים במרחב הגלובלי (דף ה-HTML). הערך של Bounds
מתאר אזור מלבני בדף, והערך של Point
מתאר קואורדינטות x,y ביחס לפינה הימנית העליונה של הדף (0,0), שנקראת גם נקודת הרישום.
בעזרת Bounds
אנחנו יכולים לזהות את הצטלבות של שני אלמנטים מלבניים (למשל, כאשר משבצת חוצה את המדף) או אם אזור מלבני (למשל, רווח בין שתי אותיות) מכיל נקודה שרירותית (למשל, נקודת המרכז של משבצת). זו ההטמעה של Bounds:
// 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(",");
}
אנחנו משתמשים ב-Point
כדי לקבוע את הקואורדינטה המוחלטת (הפינה הימנית העליונה) של כל רכיב בדף או של אירוע של העכבר. Point
מכיל גם שיטות לחישוב המרחק והכיוון, שנדרשים ליצירת אפקטים של אנימציה. זו ההטמעה של Point
:
// 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);
}
הפונקציות האלה מהוות את הבסיס ליכולות של גרירה ושחרור ואנימציה. לדוגמה, אנחנו משתמשים ב-Bounds.intersects()
כדי לקבוע אם משבצת חופפת למרחב על לוח המשחק, אנחנו משתמשים ב-Point.vector()
כדי לקבוע את הכיוון של משבצת שנגררה, ואנחנו משתמשים ב-Point.interpolate()
בשילוב עם טיימר כדי ליצור תנועה רציפה או אפקט הקטנת מהירות.
נוטה לזרום
קל יותר ליצור פריסות בגודל קבוע ב-Flash, אבל קל הרבה יותר ליצור פריסות גמישות באמצעות HTML ומודל התיבה של CSS. נבחן את תצוגת הרשת הבאה, עם רוחב וגובה משתנים:
אפשר גם להשתמש בחלונית הצ'אט. בגרסה ל-Flash נדרשו כמה פונקציות טיפול באירועים כדי להגיב לפעולות של העכבר, מסכה לאזור הגלילה, חישובים מתמטיים לחישוב מיקום הגלילה והרבה קוד אחר כדי לחבר את הכול.
לעומת זאת, גרסה HTML היא רק <div>
עם גובה קבוע ועם ערך hidden למאפיין overflow. גלילה לא עולה לנו כלום.
במקרים כאלה – משימות רגילות של פריסה – HTML ו-CSS עדיפים על Flash.
עכשיו שומעים אותי?
התקשנו להשתמש בתג <audio>
– פשוט לא ניתן היה להשמיע אפקטים קצרים של צלילים שוב ושוב בדפדפנים מסוימים. ניסינו שני פתרונות זמניים. קודם כל, הוספנו לקבצי האודיו קטעי זמן של דממה כדי להאריך אותם. לאחר מכן ניסינו להחליף בין ערוצי אודיו שונים. אף אחת מהשיטות לא הייתה יעילה או אלגנטית לחלוטין.
בסופו של דבר, החלטנו לפתח נגן אודיו משלה ב-Flash ולהשתמש באודיו ב-HTML5 כחלופה. זהו הקוד הבסיסי ב-Flash:
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, אנחנו מנסים לזהות את נגן ה-Flash המוטמע. אם הפעולה הזו נכשלת, יוצרים צומת <audio>
לכל קובץ אודיו:
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();
}
}
חשוב לדעת: האפשרות הזו פועלת רק בקובצי MP3 – אף פעם לא הטרחתנו לתמוך ב-OGG. אנחנו מקווים שהתעשייה תגיע בעתיד הקרוב להסכמה על פורמט אחד.
מיקום הסקר
אנחנו משתמשים באותה טכניקה ב-HTML5 כמו שעשינו ב-Flash כדי לרענן את מצב המשחק: כל 10 שניות, הלקוח מבקש מהשרת עדכונים. אם מצב המשחק השתנה מאז הבדיקה האחרונה, הלקוח מקבל את השינויים ומטפל בהם. אחרת, לא קורה כלום. שיטת הסקר המסורתית הזו מקובלת, אם כי לא ממש אלגנטית. עם זאת, אנחנו רוצים לעבור ל-Long Polling או ל-WebSockets ככל שהמשחק יתפתח והמשתמשים יתחילו לצפות לאינטראקציה בזמן אמת ברשת. במיוחד, WebSockets מציעים הזדמנויות רבות לשיפור חוויית המשחק.
איזה כלי!
השתמשנו ב-Google Web Toolkit (GWT) כדי לפתח גם את ממשק המשתמש של הקצה הקדמי וגם את לוגיק הבקרה של הקצה העורפי (אימות, תקינות, עקביות וכו'). קוד ה-JavaScript עצמו עובר הידור מקוד מקור של Java. לדוגמה, הפונקציה Point מותאמת מ-Point.java
:
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));
}
...
}
לכיתות מסוימות של ממשק משתמש יש קובצי תבנית תואמים שבהם רכיבי הדף 'מקושר' לחברי הכיתה. לדוגמה, ChatPanel.ui.xml
תואם ל-ChatPanel.java
:
<!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>
הפרטים המלאים לא מופיעים במאמר הזה, אבל מומלץ לבדוק את GWT בפרויקט ה-HTML5 הבא שלכם.
למה כדאי להשתמש ב-Java? קודם כול, לטיפוּס קפדני. אמנם סיווג דינמי שימושי ב-JavaScript – לדוגמה, היכולת של מערך להכיל ערכים מסוגים שונים – אבל הוא יכול להיות כאב ראש בפרויקטים גדולים ומורכבים. השנייה היא לצורך יכולות של רפקטורינג. נסו לדמיין איך משנים חתימה של שיטת JavaScript באלפי שורות קוד – לא קל! אבל עם סביבת פיתוח משולבת (IDE) טובה ל-Java, זה קל מאוד. לבסוף, למטרות בדיקה. כתיבת בדיקות יחידה לכיתות Java היא דרך טובה יותר מאשר השיטה הוותיקה של 'שמירה ורענון'.
סיכום
מלבד הבעיות באודיו, HTML5 עלה על כל הציפיות שלנו. Wordico נראה מצוין כמו ב-Flash, והוא גם זורם ורספונסיבי באותה מידה. לא היינו יכולים לעשות את זה בלי Canvas ו-CSS3. האתגר הבא שלנו: התאמת Wordico לשימוש בנייד.