跟電腦(web server)對戰的故事大概是這樣的:
- 跟 server 要求開始一場遊戲
- 跟 server 取得遊戲資訊
- 根據遊戲資訊繪製畫面
- 將玩家踩的地點傳送給 server
- 命中地雷→更新遊戲資訊
- 沒有命中→輪到 AI 踩地雷→更新遊戲資訊
- 檢查是否有某方獲勝?
- 有→結束。
- 沒有→回到步驟 3
粗體的部份是跟 server 有關的部份。
那麼,以 RPC、或說以程式的講法
server 只需要提供兩個 method
- startGame():負責開遊戲
- shoot():傳送玩家踩的地點
- 參數:uid, x, y
- 回傳:GameInfo 物件
因為懶得多設計一堆有的沒的
所以開場之後讓 client 端程式自動踩 (-1,-1) 這個非法位置
就會進入到步驟 2~4 的循環當中 [逃]
好了,終於要進入到 GWT RPC 的部份了。
這裡打算跳過理論、架構的部份
直接以程式碼來說明一切...
首先,是要有一個 MineService 的 interface
繼承自 com.google.gwt.user.client.rpc.RemoteService
裡頭就是宣告上頭說得那兩個 method
@RemoteServiceRelativePath("mineRPC")
public interface MineService extends RemoteService {
public String startGame();
public GameInfo shoot(String id, int x, int y) throws Exception;
}
至於那個 annotation 先跳過,後頭會解釋
再來是一個對應的 interface,名稱通常是後頭補 Async
public interface MineServiceAsync {
void startGame(AsyncCallback<String> callback);
void shoot(String id, int x, int y, AsyncCallback<GameInfo> callback);
}
這兩個 class 必須放在 gwt compiler 會處理的目錄下(例如 client)
有 interface 自然有實做的 class
MineService 對應實做 class 通常叫做 MineServiceImpl
會長成這樣子:
public class MineServiceImpl extends RemoteServiceServlet implements MineService {
private AI_Interface ai = new RandomAI(); //FIXME change your ai here!
@Override
public String startGame() {
String id = UUID.randomUUID().toString();
MineGM setting = new MineGM();
setServer(id, setting);
return id+",Random";
}
private void setServer(String id, MineGM setting){
this.getThreadLocalRequest().getSession().setAttribute(id+"ID", setting);
}
private MineGM getServer(String id) throws Exception{
String name = id+"ID";
if(this.getThreadLocalRequest().getSession().getAttribute(name)!=null){
return (MineGM) this.getThreadLocalRequest().getSession().getAttribute(name);
}else{
throw new Exception("還沒開局");
}
}
@Override
public GameInfo shoot(String id, int x, int y) throws Exception{
MineGM server = getServer(id);
if(x==-1 || y==-1 || server.getMap()[x][y]!=-1){
return MineGM.toGameInfo(server);
}
if(!server.shoot(x, y, MineGM.USER)){
int[] xy = new int[2];
do{
ai.guess(MineGM.toGameInfo(server), xy);
}while(server.shoot(xy[0], xy[1], MineGM.AI));
}
setServer(id, server);
return MineGM.toGameInfo(server);
}
}
這個 class 還會繼承 RemoteServiceServlet
往上追溯,parent 是 HttpServlet
也就是說,MineServiceImpl 也是一個 HttpServlet
雖然 GWT RPC 很神奇地包裝好許多東西
但終究還是 base on JSP
所以寫這個 class 時,就不用管 GWT 的重重限制
只要 web.xml 有設定正確就好
講到 web.xml,回頭講一下 MineService 的 annotation
用 RemoteServiceRelativePath 設定 servlet-mapping 會比較方便
<!-- in web.xml -->
<servlet-mapping>
<servlet-name>mineRPC</servlet-name>
<url-pattern>/_mine/mineRPC</url-pattern>
</servlet-mapping>
url-pattern 的值,前半段 _mine 是在 gwt.xml 中
設定 <module rename-to='_mine' >
後半段 mineRPC 就是在 MineService 設定的值
(沒有用這個 annotation,得要多好幾行煩死人的 code)
至於遊戲資訊,我選擇塞在 session 當中
在 RemoteServiceServlet 要取得 session 比較囉唆一點
得要這樣才能取得 session
this.getThreadLocalRequest().getSession()
其餘的程式碼... 嗯... 不在 GWT 的範圍當中,跳過 XD
server 端的程式碼解決了,現在來看 client 端
client 端必須透過 MineServiceAsync 來呼叫 RPC
不過用法有點奇怪...... Orz
首先要先這樣寫,取得一個 MineServiceAsync 的 object
MineServiceAsync msa = GWT.create(MineService.class);
然後就可以用 msa.shoot() 來告訴 server 要踩哪個位置
但是事情還沒完,除了標準的 parameter
得要傳一個為 AsyncCallback 的 parameter
這是讓 server 端處理完畢後可以 callback 的一個手段
初期通常都會用 anonymous class 來解決
所以程式碼會長得像這樣:
msa.shoot(this.gameID, hitX, hitY, new AsyncCallback<GameInfo>(){
@Override
public void onFailure(Throwable caught) {
Window.alert("shoot : "+caught.getLocalizedMessage());
}
@Override
public void onSuccess(GameInfo result) {
setShootResult(result);
}
});
這邊要注意兩件事情
其一,RPC 傳遞/回傳的 class(及其 field)
除了 primitive data type 外
一定得 implements IsSerializable
還必須是
GWT 允許的 class
此外,自訂的 class 還必須讓在 GWT compiler 會處理的目錄下
不然實際跑起來就會有(很難看懂的)錯誤訊息
其二,呼叫完 msa.shoot() 之後
下一步並不會執行 onSuccess()/onFailure()
更正確來說,在寫 client 端程式碼的時候
並不會知道 onSuccess()/onFailure() 什麼時候會呼叫到
端看 server 處理以及網路傳輸的速度... etc
這是 callback 的特性,在踩地雷的 case 當中並不會造成困擾
但在其他實務上,如果發現怎麼 RPC 回傳值都不正確
那大概就是忘記這個性質所導致的...... Orz
喔對... 都忘記講一個大前提了
要用 GWT RPC,server 必須是 JSP container
同時也來講講 GWT RPC 的好處 \囧/
一言以蔽之就是「
通通傳便便」
client 不用組 query string 或 post 內容
server 端不用準備對應的 url
client/server 都不需要剖析傳遞的資料
甚至可以傳遞(客製化的) exception
在 client 端的 onFailure() 可以分門別類處理......
程式碼看起來、寫起來都很 OO、都很 Java
以一個 Java Programmer 來說,有什麼比這個更快樂的事情呢? XD
(連 xml 都沒有用到呢! [握拳])
好了,「
對抗電腦版的踩地雷」拆解到這裡
只剩下地雷區要設定 click 的 handler
收到 GameInfo 之後要更新畫面
以及電腦 AI 設計這些功能
相信你一定可以自己寫的很開心的,就跳過不細談了
如果你想偷懶想拿寫好的程式碼來執行看看,
可以到
這裡下載。也歡迎
投稿你的 AI 設計
Enjoy GWT and have fun! \囧/