แน่นอน ก็ต้องเป็นเกมส์ Hangman
จะได้เอาไปโชว์ในงาน CNUG ด้วย
โดยผมจะลอง implment โดยแยก object ในเกมส์ เป็น component ย่อยๆ
เพื่อจะได้ทดลอง feeling ของการทำ modular ใน Dojo
จัดแบ่ง component ออกเป็น
+ cnug
+ hangman
- Game.js // domain object
- Graphics.js // ส่วนแสดงรูป
- Unreveal.js // ส่วนแสดงคำที่ให้ทาย
- Letters.js // ส่วนแสดงตัวอักษรให้กดเลือก
+ assets // เก็บภาพ
- ...png
ขั้นแรกสุด ลองมองที่ภาพรวมก่อน เวลาเอา module มาประกอบกัน,
หน้าตาของ file hangman.html เฉพาะส่วน body จะเป็นแบบนี้
<body>
<center>
<h1>Hangman</h1>
<div dojoType="cnug.hangman.Graphics"></div>
<div id="unreveal" class="letters" ></div>
<br/>
<div id="letters1" class="letters" dojoType="cnug.hangman.Letters" chars="abcdefghijklm" ></div>
<div id="letters2" class="letters" dojoType="cnug.hangman.Letters" chars="nopqrstuvwxyz" ></div>
<br/>
<div id="button" dojoType="dijit.form.Button" onclick="newGame()">New Game</div>
</center>
</body>
จะเห็นว่าเรานำ component มาวางต่อกันดังนี้
Graphics
Unreveal
Letters
Letters
Button
ใน component ที่นำมาวางข้างบน แยกเป็นสองพวกตามวิธี initialize widget,
ก็คือ พวกที่ initialize โดยวิธี declarative (โดยการระบุ attribute dojoType ลงไป)
กับพวกที่ initialize โดยวิธี programing
ลองดู javascript code ส่วนที่ทำหน้าที่ initialize โปรแกรม
// register module path
dojo.registerModulePath("cnug","../../cnug");
// require component
dojo.require("dojo.parser");
dojo.require("cnug.hangman.Game");
dojo.require("cnug.hangman.Unreveal");
dojo.require("cnug.hangman.Letters");
dojo.require("cnug.hangman.Graphics");
dojo.require("dijit.form.Button");
// initialize
dojo.addOnLoad(function() {
game = new cnug.hangman.Game();
var unreveal = new cnug.hangman.Unreveal({game: game}, dojo.byId("unreveal"));
dojo.connect(dijit.byId("letters1"), "select", game, "guess");
dojo.connect(dijit.byId("letters2"), "select", game, "guess");
newGame();
});
การ registerModulePath จะช่วยให้เราสามารถจัดแยก code ที่เราเขียน
กับ code ที่ dojo provide มาให้ เพื่อให้สะดวกต่อการ maintain version control ของโปรเจค
ขั้นตอนการทำงานหลักๆ ก็คือ
- new Game object
- สร้าง unreveal Component โดยผ่านค่า game instance เข้าไปทาง constructor ของ Unreveal
- เชื่อม event ระหว่าง Letters component เข้ากับ Game instance
เพื่อที่เวลา user กดเลือกตัวอักษร, game instance จะได้ update state ตัวเอง - เรียก function newGame ที่จะทำหน้าที่ ajax call ไปขอคำศัพท์ จาก server
และ set คำศัพท์นั้นให้กับ Game object
ลองมองเข้าไปใน Component แต่ละตัวบ้าง
เริ่มที่ Component Game ก่อน
หน้าตาของ Game.js เป็นแบบนี้
dojo.provide("cnug.hangman.Game");
dojo.declare("cnug.hangman.Game", null, {
newGame: function(word) {
this.word = word
this.unreveal = word.replace(/./g, '_')
this.mistake = 0;
this._fireNewGame();
},
guess: function(ch) {
var idx = this.word.indexOf(ch);
if (idx >= 0) {
var tmp = '';
for (var i = 0; i < this.word.length; i++) {
if (this.word.charAt(i) == ch) {
tmp += ch;
} else {
tmp += this.unreveal.charAt(i);
}
}
this.unreveal = tmp;
} else {
this.mistake++;
}
this._fireChange();
},
isWin: function() {
return this.unreveal.indexOf('_') < 0;
},
isLose: function() {
return this.mistake > 5;
},
_fireChange: function(){
dojo.publish("stateChange", [this]);
},
_fireNewGame: function(){
dojo.publish("newGame", [this]);
}
});
ประเด็นที่น่าสนใจก็มี
dojo.provide
ก็คือคำสั่งที่ระบุว่า file นี้เป็น module อะไรdojo.declare
ก็คือ คำสั่งที่ใช้สร้าง Class Person (OOP ใน javascript เป็นพวก prototype base)- จะเห็นว่า method จะไม่มี Visiblilty scope กำกับ
ทุกอย่างเป็น public หมด
เราก็เลยใช้วิธีตั้งชื่อ อันไหนที่เป็น internal ก็นำหน้าด้วย underscore เสีย - การสื่อสารระหว่าง Game Object กับ component อื่นๆ
ใช้ feature Publish-Subscribe ของ Dojo
ลองดู component ที่เป็น UI Widget บ้าง
เริ่มที่ Letters.js ที่ทำหน้าที่สร้างแถวตัวอักษรที่ user สามารถเลือก click ได้
และเมื่อ click แล้ว ตัวอักษรนั้น ก็จะ disabled ไม่ให้เลือกซ้ำอีก
ลองดูตัวเล็กสุดก่อน นั่นก็คือ LetterLink ที่ represent ตัวอักษรแต่ละตัว
dojo.declare("cnug.hangman.LetterLink", dijit.form._FormWidget,
{
char: '',
parent: null,
templateString: "<span><a href='#' dojoAttachPoint='anchor'
dojoAttachEvent='onclick: _select'>${char}</a> </span>",
_select: function() {
if (! this.disabled) {
this.setDisabled(true);
dojo.addClass(this.anchor, "disabledLink");
this.parent.select(this.char);
}
},
_reset: function() {
this.setDisabled(false);
dojo.removeClass(this.anchor, "disabledLink");
}
});
ประเด็นที่น่าสนใจ
- วิธีการ declare Object ที่ extends จาก Object อื่นๆ
เราสามารถใช้คำสั่งdojo.delare("myclass", subclass, { // class body });
หรือถ้า inherrit มาจากหลาย object ก็ใช้dojo.delare("myclass", [subclass1,subclass2], { // class body });
(ใช้คำว่า class เพื่อให้คนคุ้นกับ java เข้าใจ, แต่จริงๆมันคือ object หรือ function) - เราใช้ Object "dijit.form._FormWidget" เป็นต้นแบบ
เนื่องจากมันมี คุณสมบัติหลายอย่างที่เราสามารถนำมาใช้ได้เลย เช่น disable, enable - templateString ก็คือการกำหนดว่าหน้าตาของ html ที่เรา generate ออกไปจะเป็นอย่างไร
ใน dojo, เราสามารถแทรก attachPoint, attachEvent ใน template ได้
อย่างใน code ข้างบน เรามี dojoAttachPoint เป็นค่า "anchor"
หมายความว่า Dojo จะ inject DOM นั้นลงใน property ที่ชื่อ "anchor" ให้เราโดยอัตโนมัติ
ทำให้เราสามารถเขียน this.anchor.src = 'xx' ได้เลย
ส่วน dojoAttachEvent ทำให้เราร้อย event จาก DOM
เข้ากับ function ใน Object เราได้ง่ายๆ
ย้อนกลับมาดู Letters ที่ทำหน้าที่เป็น container ของ LetterLink บ้าง
dojo.declare("cnug.hangman.Letters", [dijit._Widget, dijit._Container],
{
chars: '',
charsWidget: null,
postMixInProperties: function() {
this.charsWidget = [];
},
postCreate: function() {
var cs = this.chars.split('');
for (var i=0; i < cs.length; i++) {
this.charsWidget[i] = new cnug.hangman.LetterLink({char: cs[i], parent: this});
this.addChild(this.charsWidget[i]);
}
dojo.subscribe("newGame", this, "reset");
},
reset: function() {
dojo.forEach(this.charsWidget, function(elm) {
if (elm.disabled) {
console.debug("reset" + elm);
elm._reset();
}
});
},
select: function(ch) { console.debug(ch);}
});
ประเด็นน่าสนใจคือ
- object นี้ inherrit จาก dijit._Widget และ dijit_Container
คือเป็นทั้ง Widget ที่แสดงบนหน้าจอ และสามารถ container Widget ตัวอื่นๆได้ - method ของ dijit._Widget ที่เรา override ก็คือ
postMixinProperties ที่จะถูกเรียกหลังจากที่มีเสร็จสิ้นการ set property ต่างๆให้ object
ส่วน postCreate ถูกเรียกใช้ เมื่อสิ้นสุดขบวนการสร้าง Widget แล้ว - select function เป็น function เปล่าๆ
ใช้เป็นจุด extension point ให้สามารถร้อย function นี้
เข้ากับ function ของ Object อื่นๆ ตามที่เราต้องการ
(จะใช้กลไก publish-subscribe ก็ได้ แต่เบื่อแล้ว ก็เลยลองอย่างอื่นบ้าง) - พวก chars, charsWidget เป็น property ของ Object Letters
ประเด็นที่ต้องระวังก็คือ พวก property ที่ชี้ไปยัง object (พวกที่เป็น by reference)
จะต้อง initialize ใน postMixinProperties มิเช่นนั้น มันจะกลายเป็น
static reference ที่ share กันระหว่าง object instance ของ Letters
ลองดูตัวอย่างจาก code ง่ายๆนี้dojo.declare("Person",null, {
item: [] // กลายเป็น static reference
}
p1 = new Person();
p2 = new Person();
p1.item[0] = 1;
p2.item[1] = 2;
alert(p1.item); // => [1,2]
ชักจะยาวไปแล้ว
ใครสนใจดูรายละเอียด ไปคุยกันได้ที่งาน CNUG ครับ
Note: ตอนแรกว่าจะลองเขียนด้วย YUI ด้วย
แต่พบว่า YUI เขียนเป็น modular แล้วไม่สนุกเท่า Dojo
ก็เลยเลิก
3 comments:
สบายดีไหมพี่ป๊อก ไม่ได้เจอกันนานเลย งาน BTD 3 ผมก็ไม่ได้ไปเสียดาย ๆๆๆ
หายเงียบไปเลย
ช่วงนี้ทำอะไรอยู่
เขียน mail มาเล่าใ้ห้ฟังบ้าง
(mail พี่ ก็ pphetra AT gmail)
ไม่ทราบว่ามีสาวๆ หรือเปล่าครับ อยู่ใกล้บริษัทแค่นี้เอง อ่ะ
จะไปร่วมแจมด้วยจะได้หรือเปล่าครับ
ปู่
Post a Comment