บทนำ
เมื่อเราแปลงเกมไขปริศนาคำไขว้ Wordico จาก Flash เป็น HTML5 งานแรกของเราคือการเลิกเรียนรู้ทุกสิ่งที่รู้เกี่ยวกับการสร้างประสบการณ์การใช้งานที่สมบูรณ์ในเบราว์เซอร์ ในขณะที่ Flash มี API เดียวที่ครอบคลุมการพัฒนาแอปพลิเคชันทุกด้าน ตั้งแต่การวาดเวกเตอร์ไปจนถึงการตรวจหาการตีโพลิก้อนไปจนถึงการแยกวิเคราะห์ XML แต่ HTML5 มีข้อกำหนดที่หลากหลายซึ่งรองรับเบราว์เซอร์แตกต่างกันไป นอกจากนี้ เรายังสงสัยว่า HTML ซึ่งเป็นภาษาเฉพาะเอกสารและ CSS ซึ่งเป็นภาษาที่เน้นกล่องเหมาะกับการสร้างเกมหรือไม่ เกมจะแสดงอย่างสม่ำเสมอในเบราว์เซอร์ต่างๆ เช่นเดียวกับใน Flash ไหม และเกมจะดูและทำงานได้ดีเท่าเดิมไหม สำหรับ Wordico คำตอบคือใช่
คุณ Victor โปรดระบุเวกเตอร์
เราพัฒนา Wordico เวอร์ชันแรกโดยใช้เฉพาะกราฟิกเวกเตอร์ เช่น เส้น โค้ง การเติม และไล่ระดับ ผลลัพธ์ที่ได้คือทั้งกะทัดรัดและปรับขนาดได้แบบไม่จำกัด
นอกจากนี้ เรายังใช้ประโยชน์จากไทม์ไลน์ของ Flash เพื่อสร้างออบเจ็กต์ที่มีหลายสถานะ ตัวอย่างเช่น เราใช้คีย์เฟรมที่มีชื่อ 9 รายการสำหรับออบเจ็กต์ Space
ดังนี้
แต่จะใช้สไปรท์แบบบิตแมปใน HTML5
หากต้องการสร้างกระดานเกมขนาด 15x15 จากช่องแต่ละช่อง เราจะวนซ้ำการเขียนสตริง 225 อักขระ โดยแต่ละช่องจะแสดงด้วยอักขระที่แตกต่างกัน (เช่น "t" สำหรับตัวอักษร 3 ตัว และ "T" สำหรับคำ 3 คำ) ซึ่งการดำเนินการนี้ทำได้ง่ายมากใน 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>
ซึ่งเป็นพื้นผิวการวาดภาพบิตแมปเพื่อวาดกระดานเกมทีละช่อง ขั้นตอนแรกคือโหลดสไปรท์รูปภาพ เมื่อโหลดแล้ว เราจะวนดูการเขียนแทนที่เลย์เอาต์ โดยวาดส่วนต่างๆ ของรูปภาพในแต่ละรอบดังนี้
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 เราจะรวมสไปรต์รูปภาพ 3 รายการไว้ในองค์ประกอบ <canvas>
รายการเดียวที่รันไทม์ ดังนี้
ตอนนี้เรามีภาพพิมพ์แคนวาส 100 ภาพ (1 ภาพสำหรับแต่ละไทล์) บวกภาพพิมพ์แคนวาสสำหรับกระดานเกม มาร์กอัปสำหรับไทล์ "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 เมื่อมีการลากการ์ด (เงา ความทึบแสง และการปรับขนาด) และเมื่อการ์ดอยู่บนชั้นวาง (แสงสะท้อน) ดังนี้
การใช้รูปภาพแรสเตอร์มีข้อดีบางอย่างที่เห็นได้ชัด ประการแรก ผลลัพธ์จะมีความแม่นยำระดับพิกเซล ประการที่ 2 เบราว์เซอร์อาจแคชรูปภาพเหล่านี้ไว้ ประการที่สาม เราสามารถเปลี่ยนรูปภาพเพื่อสร้างการออกแบบใหม่ได้ เช่น กระเบื้องโลหะ และงานออกแบบนี้ทำได้ใน Photoshop แทนที่จะทำใน Flash
ข้อเสียคืออะไร การใช้รูปภาพหมายความว่าเราจะยกเลิกการเข้าถึงแบบเป็นโปรแกรมในช่องข้อความ ใน Flash การเปลี่ยนสีหรือพร็อพเพอร์ตี้อื่นๆ ของประเภทนั้นทําได้ง่ายๆ แต่ HTML5 นั้นพร็อพเพอร์ตี้เหล่านี้จะฝังอยู่ในรูปภาพ (เราลองใช้ข้อความ HTML แล้ว แต่ต้องใช้มาร์กอัปและ CSS เพิ่มเติมเป็นจำนวนมาก เราลองใช้ข้อความแคนวาสด้วย แต่ผลลัพธ์ที่ได้ไม่สอดคล้องกันในเบราว์เซอร์ต่างๆ)
ตรรกะแบบคลุมเครือ
เราต้องการใช้ประโยชน์จากหน้าต่างเบราว์เซอร์อย่างเต็มที่ไม่ว่าจะมีขนาดเท่าใดก็ตาม และหลีกเลี่ยงการเลื่อน ซึ่งการดำเนินการนี้ค่อนข้างง่ายใน Flash เนื่องจากเกมทั้งหมดวาดด้วยเวกเตอร์และสามารถปรับขนาดขึ้นหรือลงได้โดยไม่สูญเสียความคมชัด แต่การทำใน HTML จะยากกว่า เราลองใช้การปรับขนาด CSS แต่ภาพพิมพ์แคนวาสกลับเบลอ
โซลูชันของเราคือการวาดกระดานเกม แร็ค และไพ่ใหม่ทุกครั้งที่ผู้ใช้ปรับขนาดเบราว์เซอร์ โดยทำดังนี้
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
ผลลัพธ์ที่ได้คือรูปภาพที่คมชัดและเลย์เอาต์ที่สร้างความพึงพอใจในทุกขนาดหน้าจอ
เข้าประเด็นทันที
เนื่องจากการ์ดแต่ละใบมีตำแหน่งที่แน่นอนและต้องจัดวางให้สอดคล้องกับกระดานเกมและขาตั้งอย่างแม่นยำ เราจึงต้องใช้ระบบการวางตำแหน่งที่เชื่อถือได้ เราใช้ฟังก์ชัน 2 รายการ ได้แก่ Bounds
และ Point
เพื่อช่วยจัดการตําแหน่งองค์ประกอบในพื้นที่ส่วนกลาง (หน้า HTML) Bounds
อธิบายพื้นที่สี่เหลี่ยมผืนผ้าบนหน้าเว็บ ส่วน Point
อธิบายพิกัด x,y ที่สัมพันธ์กับมุมซ้ายบนของหน้า (0,0) หรือที่เรียกว่าจุดลงทะเบียน
เมื่อใช้ Bounds
เราจะตรวจหาจุดตัดขององค์ประกอบสี่เหลี่ยมผืนผ้า 2 รายการ (เช่น เมื่อการ์ดวางทับแร็ค) หรือพื้นที่สี่เหลี่ยมผืนผ้า (เช่น พื้นที่ว่างระหว่าง 2 อักขระ) มีจุดที่กำหนดเองหรือไม่ (เช่น จุดศูนย์กลางของการ์ด) การใช้งาน 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 Box Model พิจารณามุมมองตารางกริดต่อไปนี้ที่มีความกว้างและความสูงแบบแปรผัน
หรือลองใช้แผงแชท เวอร์ชัน Flash ต้องใช้ตัวแฮนเดิลเหตุการณ์หลายรายการเพื่อตอบสนองต่อการทำงานของเมาส์ มาสก์สําหรับพื้นที่ที่เลื่อนได้ คณิตศาสตร์สําหรับคํานวณตําแหน่งการเลื่อน และโค้ดอื่นๆ อีกมากมายเพื่อเชื่อมโยงเข้าด้วยกัน
ในทางกลับกัน เวอร์ชัน HTML เป็นเพียง <div>
ที่มีความสูงคงที่และตั้งค่าพร็อพเพอร์ตี้การรองรับค่าที่เกินไว้เป็น "ซ่อน" การเลื่อนไม่มีค่าใช้จ่าย
ในกรณีเช่นนี้ ซึ่งเป็นงานการจัดวางธรรมดา HTML และ CSS จะมีประสิทธิภาพมากกว่า Flash
ตอนนี้ได้ยินฉันไหม
เราพบปัญหาเกี่ยวกับแท็ก <audio>
เนื่องจากเล่นเสียงเอฟเฟกต์สั้นๆ ซ้ำๆ ในบางเบราว์เซอร์ไม่ได้ เราได้ลองใช้วิธีแก้ปัญหา 2 วิธีแล้ว ก่อนอื่น เราเพิ่มช่วงพักระหว่างเสียงลงในไฟล์เสียงเพื่อให้ยาวขึ้น จากนั้นเราลองสลับการเล่นในช่องเสียงหลายช่อง เทคนิคทั้งสองไม่ได้มีประสิทธิภาพหรือมีประสิทธิภาพอย่างสมบูรณ์
สุดท้ายเราตัดสินใจที่จะเปิดตัวโปรแกรมเล่นเสียง 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 เมื่อเกมพัฒนาขึ้นและผู้ใช้คาดหวังการโต้ตอบแบบเรียลไทม์ผ่านเครือข่าย โดยเฉพาะอย่างยิ่ง WebSocket จะเปิดโอกาสมากมายในการปรับปรุงการเล่นเกม
เครื่องมือที่ยอดเยี่ยม
เราใช้ 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));
}
...
}
คลาส UI บางคลาสมีไฟล์เทมเพลตที่สอดคล้องกันซึ่งองค์ประกอบหน้าเว็บ "เชื่อมโยง" กับสมาชิกของคลาส ตัวอย่างเช่น 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 เช่น ความสามารถของอาร์เรย์ในการเก็บค่าประเภทต่างๆ แต่อาจทำให้เกิดปัญหาในโปรเจ็กต์ขนาดใหญ่ที่ซับซ้อน ข้อที่ 2 สำหรับความสามารถในการเปลี่ยนรูปแบบ ลองนึกถึงวิธีที่คุณจะเปลี่ยนลายเซ็นเมธอด JavaScript ในโค้ดหลายพันบรรทัด ซึ่งไม่ใช่เรื่องง่าย แต่ IDE ของ Java ที่ดีจะช่วยให้คุณทำสิ่งเหล่านี้ได้อย่างรวดเร็ว สุดท้ายนี้ เพื่อการทดสอบ การเขียนการทดสอบ 1 หน่วยสําหรับคลาส Java ดีกว่าเทคนิค "บันทึกและรีเฟรช" ที่รู้จักกันมาอย่างยาวนาน
สรุป
ยกเว้นปัญหาเกี่ยวกับเสียง HTML5 ทำงานได้เกินความคาดหมายของเราอย่างมาก Wordico ไม่เพียงแต่จะดูดีเหมือนใน Flash เท่านั้น แต่ยังลื่นไหลและตอบสนองได้ดีไม่แพ้กัน เราคงทำไม่ได้หากไม่มี Canvas และ CSS3 ปัญหาถัดไปของเราคือการปรับ Wordico ให้เหมาะกับการใช้งานบนอุปกรณ์เคลื่อนที่