[TOC]
開始毫無頭緒,找了幾個專門做游戲的框架,layabox 裝了打開報錯、cocos studio 感覺太復雜,phaser感覺引用資源好復雜。
最后我感覺我這個小游戲沒啥精靈動畫,應該普通h5+js能實現。就拿了目前最熟悉可以雙向綁定的vue來弄。
開發的時候感覺沒效率,不知道游戲場景里放什么,后來干脆用墨刀設計了一個[原型](https://modao.cc/app/kyykUyNnY6dZUCWvfUAykOIfwhpkR2y)。
# UI設計
主要設計了歡迎頁、游戲頁、結束頁 三個場景。



# Vue的使用
## Vue 的生命周期

jquery 以前有個ready 事件(所有元素加載完)。vue上是什么一直沒搞懂。
后來經過我的研究,是*mounted*。
還有加載前的事件 *beforeCreate*。
## Vue的路由研究
開始想場景相當于spa應用的單頁面。但是一看到那個vue-router 安裝要npm 就煩。感覺別人的實現也就div的顯隱。
后來想html5 不是有個 history api。用于監聽url變化的。
經過實驗,history api 監聽地址改變,動態將當前頁設為錨點對應的方法是可以的。
~~~
beforeCreate:function(){
var isHistoryApi = !!(window.history && history.pushState);
if(!isHistoryApi){
alert('該瀏覽器不支持History,請換一個現代化瀏覽器');
}
},
mounted :function(){
const that = this;
window.onpopstate = function(event){
// console.log(location);
// console.log(event);
var next_page = location.hash.substr(1);
that.page = next_page;
that.message = next_page;
}
try {
if ("WebSocket" in window) {
ws = new WebSocket("ws://"+that.host);
} else if ("MozWebSocket" in window) {
ws = new MozWebSocket("ws://"+that.host);
}
SocketCreated = true;
isUserloggedout = false;
ws.onopen = that.wsopen;
ws.onmessage = that.wsmsg;
ws.onclose = that.wsclose;
ws.onerror = that.wserror;
if(this.page == 'welcome'){
}
} catch (ex) {
console.log(ex);
alert('該瀏覽器不支持Websocket,請換一個現代化瀏覽器');
return;
}
}
~~~
試圖里直接v-if
`<section v-if="page == 'gaming'" id="gaming">`
開始用的template 標簽,結果vue 里自定義標簽會報warning。后來就改成section了。
## Vue的websocket使用
websocket 我用過,問題它怎么于vue 結合起來。后來找了一圈vue 好像有套件。可是安裝又比較麻煩。我就想想辦法把。
最終研究出來的方式是,定義一個ws變量,mounted 時候鏈接一下。
var ws;
~~~
mounted :function(){
const that = this;
window.onpopstate = function(event){
// console.log(location);
// console.log(event);
var next_page = location.hash.substr(1);
that.page = next_page;
that.message = next_page;
}
try {
if ("WebSocket" in window) {
ws = new WebSocket("ws://"+that.host);
} else if ("MozWebSocket" in window) {
ws = new MozWebSocket("ws://"+that.host);
}
SocketCreated = true;
isUserloggedout = false;
ws.onopen = that.wsopen;
ws.onmessage = that.wsmsg;
ws.onclose = that.wsclose;
ws.onerror = that.wserror;
if(this.page == 'welcome'){
}
} catch (ex) {
console.log(ex);
alert('該瀏覽器不支持Websocket,請換一個現代化瀏覽器');
return;
}
}
~~~
然后這些ws用的方法通通以ws開頭,比如wsopen。
# 整體前端架構
~~~
wslogin:function(name){
this.wssend({op:'reg_user', nickname:name});
},
wsbegin:function(room_id, uid){
this.wssend({
op:"begin", room_id: room_id, uid:uid
});
},
wsopen: function(){
if(this.page == 'gaming' && this.name != ''){
this.wslogin(this.name);
}
},
wssend: function(msg){
if(ws){
if(typeof msg !== 'string'){
msg = JSON.stringify(msg);
}
ws.send(msg);
}else{
this.wserror();
}
},
wsmsg : function(event){
try {
console.log(event);
var obj = JSON.parse(event.data);
console.log(obj);
if(obj.code == 0){
alert(obj.msg);
}
console.log(obj.msg);
console.log(obj.data);
switch(obj.msg){
case 'after_reg_user':
this.uid = obj.data.uid;
this.fd = obj.data.fd;
localStorage.setItem('uid', this.uid);
localStorage.setItem('nickname', obj.data.nickname);
if(!this.room_id){
if(localStorage.getItem('room_id') == undefined){
this.wssend({
op: 'create_room',
name: 'room'+ new Date().getTime() + Math.random(),
hours: 1,
uid: this.uid,
number: 2
});
}else{
this.room_id = localStorage.getItem('room_id');
}
}
if(this.fd && this.uid && this.room_id && this.get_new('compete_uid')){
this.start();
}
break;
case 'after_create_room':
this.room_id = obj.data.room_id;
localStorage.setItem('room_id', this.room_id);
if(!this.get_new('compete_uid') && this.uid && this.room_id){
this.wsbegin(this.room_id, this.uid);
}
break;
case 'after_begin':
this.compete_uid = obj.data.compete_uid;
localStorage.setItem('compete_uid', this.compete_uid);
this.user.name = obj.data.user_name;
this.compete_user.name = obj.data.compete_name;
this.start();
break;
case 'room_user_list':
if(this.room_id == obj.data.room_id){
for (var i = 0; i < obj.data.list.length; i++) {
var user = obj.data.list[i];
if(user.uid == this.uid){
this.user.stars = user.stars;
}
if(user.uid == this.compete_uid){
this.compete_user.stars = user.stars;
}
obj.data.list[i];
}
this.count_down_cards = obj.data.count_down_cards;
}
break;
case 'after_enter_room':
for (var i = 0; i < obj.data.list.length; i++) {
var user = obj.data.list[i];
if(user.uid == this.uid){
this.user.stars = user.stars;
}
if(user.uid == this.compete_uid){
this.compete_user.stars = user.stars;
}
obj.data.list[i];
}
this.user.name = obj.data.user_name;
this.user.石頭 = obj.data.石頭;
this.user.剪刀 = obj.data.剪刀;
this.user.布 = obj.data.布;
this.compete_user.name = obj.data.compete_name;
this.count_down_cards = obj.data.count_down_cards;
break;
case 'after_do_guess':
this.user.石頭 = obj.data.left_cards.石頭;
this.user.剪刀 = obj.data.left_cards.剪刀;
this.user.布 = obj.data.left_cards.布;
switch(obj.data.compete_type){
case '石頭':
this.compete_type = 'shitou';
break;
case '剪刀':
this.compete_type = 'jiandao';
this.user.剪刀 = obj.data.left_cards.剪刀;
break;
case '布':
this.compete_type = 'bu';
this.user.布 = obj.data.left_cards.布;
break;
}
// this.result = obj.data.result;
var that = this;
setTimeout(function(){
var dialog = new Dialog({
onRemove:function(){
that.result = '';
that.compete_type = 'back';
that.user.checked = '';
}
}).alert('U ' +obj.data.result, {
type:'remind',
})
}, 200)
break;
case 'win':case 'draw':case'lose':
this.end_status = obj.msg;
this.room_id = 0;
this.compete_uid = 0;
this.egg_name = obj.data.egg_name;
localStorage.removeItem('room_id');
localStorage.removeItem('compete_uid');
location.href = '#end';
break;
case 'notify':
this.notify = obj.data.info;
break;
default:
;
}
} catch (error) {
console.log(error);
alert('錯誤的服務器消息');
} finally {
}
},
wsclose: function(){
this.fd = 0;
ws.close();
},
wserror: function(){
alert('網絡鏈接失敗');
if(this.page == 'gaming' || this.page == 'end'){
location.href = '/#welcome';
location.reload(true);
}
}
~~~
wsmsg處里消息。
wssend來發送消息。
定義了發消息的格式 json,帶op,和后端對應,然后wsmsg里,所有消息對應的響應方法都有`after_ `前綴。
然后根據這些事件的響應,更新ui 比如卡牌倒計時、每個人的星星等。
# 初始化數據
~~~
data: {
notify: '歡迎進入比賽',
page: 'welcome',
end_status: 'Win',
egg_name: '',
host: '127.0.0.1:9502',
uid: 0,
fd: 0,
room_id: 0,
compete_uid: 0,
guess_type: '',
compete_type: 'back',
user: {
name: '',
stars: 3,
checked: '',
'石頭': 4,
'剪刀': 4,
'布': 4,
},
compete_user: {
name: '',
stars:3,
},
count_down_cards: {'石頭':8,'剪刀':8,'布':8},
},
~~~
一些預設配置,一些動態比如用戶和對手信息,最新消息等。
# 緩存
在做的過程中,發現用戶有刷新當前頁面的習慣。所以只要之前的游戲沒結束,就要回到刷新前的狀態。后來就用localStorage,來持久化信息。當然也加了一些消息用于主動獲取信息,如enter_room。
# 刷新后保持狀態和最新數據
一開始,卡牌倒計時是每次出過牌后才返回來。但是如果恢復場景,就不知道,所以加了個enter_room消息。
# UI 變為場景
開始我打算樣式一個個手寫。后來發現墨刀的預覽效果上,都是html+css實現的。

于是就復制,去掉無用id class屬性。把所有定位調整相對定位。然后通過調整top、left的方式總算把靜態部分實現了。
暫時不做自適應所有屏幕尺寸把。
# 當前剩余卡牌為0 的出牌無效
當時出牌沒判斷自己剩余的該卡牌是否還有剩余數量。
然后after_do_guess里加了*left_cards*
~~~
$left_cards = [
'石頭' => RoomUserCards::left_card('石頭', $data['room_id'], $data['uid']),
'剪刀' => RoomUserCards::left_card('剪刀', $data['room_id'], $data['uid']),
'布' => RoomUserCards::left_card('布', $data['room_id'], $data['uid'])
];
$this->success('after_do_guess', ['result' => $result,'compete_type'=>$compaire_card['type'], 'left_cards'=>$left_cards]);
~~~
這樣每次出牌后我都更新user對象里的各種牌型的剩余數量。發現瀏覽器里中文js對象索引頁支持。
然后對應視圖里做判斷:
~~~
<div class="widget image_view user_card" id="user_shitou" v-bind:class="{checked :user.checked == '石頭', disabled: user.石頭 == 0}" @click="choose('石頭')"></div>
<div class="widget image_view user_card" id="user_jiandao" v-bind:class="{checked :user.checked == '剪刀', disabled: user.剪刀 == 0}" @click="choose('剪刀')"></div>
<div class="widget image_view user_card" id="user_bu" v-bind:class="{checked :user.checked == '布', disabled: user.布 == 0}" @click="choose('布')"></div>
~~~
# 一次判定后的處理
每次出牌后有結果后,就會將checked 重置。不讓其高亮選擇狀態。
開始界面想搞一個自動進入游戲,如果緩存的參數都在的情況下。
后來發現用vue的 watch 就可以了。
~~~
watch: {
page: function (val) {
if(val == 'gaming'){
if(this.uid && this.compete_uid){
this.wssend({
op: 'enter_room',
room_id: this.room_id,
uid: this.uid,
compete_uid: this.compete_uid,
});
}
}else if(val == 'welcome'){
this.start();
}
},
},
~~~
當頁面發生切換時。
# 其他方法
start、get_new、choose、 again。
start 就是點擊開始按鈕,會做很多判斷。
get_new 就是老是被問是不是老婆知道。
choose 就是do_guess。
again 結束當前房間再來一次。
具體看完整源碼即可。
# 視圖
~~~
<div id="app">
<section v-if="page == 'welcome'" id="welcome">
<div id="title">
賭一把
<br>
</div>
<div class="widget button hcenter vmiddle clickable animated flash" id="start_btn" @click="start()">
<div class="button-wrapper">
<span class="text">開始</span>
</div>
</div>
<div class="widget text_view hleft vtop" id="desc">
<div class="text" style="padding: 10px;"><p>游戲規則:初始進入創建角色后,再創建房間,每人初始3顆星,進行猜拳游戲,手中四組“石頭、剪刀、布”。雙方互相決定出什么牌后,翻面判定本次出牌勝負。每次出牌有 win 、lose、draw。三個結果。出過的牌作廢。win獲得對方的一顆星,平局不消耗星。輸了減少一顆星給對方。有人輸光3顆星或者牌走完,游戲結束。</p>
</div>
</div>
<div class="widget label hcenter vmiddle" id="footer"><p>本游戲由jaylabs實驗室老楊提供</p></div>
</section>
<section v-if="page == 'gaming'" id="gaming">
<div class="widget rounded_rect hcenter vmiddle" id="notify"><p v-show="notify !=''"> 通知:{{notify}}</p></div>
<div class="widget rounded_rect hleft vmiddle name" id="user_info_name" style="padding: 0px;"><p>{{user.name}}</p></div>
<div class="widget rounded_rect hleft vmiddle name" id="compete_info_name" style="padding: 0px;"><p>{{compete_user.name}}</p></div>
<div class="count_wraper">
<div class="widget image_view countdown" id="countdown_shitou"></div>
<div class="widget image_view countdown" id="countdown_jiandao"></div>
<div class="widget image_view countdown" id="countdown_bu"></div>
</div>
<div class="star_wraper">
<img src="static/img/star.png" v-for="n in user.stars">
</div>
<div class="star_wraper" id="compete_stars">
<img src="static/img/star.png" v-for="n in compete_user.stars">
</div>
<div class="countdown_num" id="countdown_num_shitou">{{count_down_cards.石頭}}</div>
<div class="countdown_num" id="countdown_num_jiandao">{{count_down_cards.剪刀}}</div>
<div class="countdown_num" id="countdown_num_bu">{{count_down_cards.布}}</div>
<div class="widget image_view" v-bind:class="[compete_type]" id="compete_type"></div>
<div class="widget image_view user_card" id="user_shitou" v-bind:class="{checked :user.checked == '石頭', disabled: user.石頭 == 0}" @click="choose('石頭')"></div>
<div class="widget image_view user_card" id="user_jiandao" v-bind:class="{checked :user.checked == '剪刀', disabled: user.剪刀 == 0}" @click="choose('剪刀')"></div>
<div class="widget image_view user_card" id="user_bu" v-bind:class="{checked :user.checked == '布', disabled: user.布 == 0}" @click="choose('布')"></div>
</section>
<section v-if="page == 'end'" id="end">
<div class="widget rounded_rect hcenter vmiddle" id="result">
<div class="text" style="padding: 0px;">
<p><font color="#ce1919">U {{end_status}}</font></p>
<p class="egg_text" v-show="egg_name">我找到了我的妞,{{egg_name}}!</p>
</div>
</div>
<div class="widget button hcenter vmiddle animated fadeOut" id="again_btn">
<div class="button-wrapper"><span class="text" @click="again()">再來一把</span></div>
</div>
</section>
</div>
~~~
整個視圖也就3個section。id分別為welcome、gaming、end。
## v-for n in
顯示星星的時候,想用v-for 但是一般都是定義一個數組,我只想循環1-3 這種,終于被我在手冊里找到了。
~~~
<div class="star_wraper" id="compete_stars">
<img src="static/img/star.png" v-for="n in compete_user.stars">
</div>
~~~
## 結束頁的背景
因為背景圖是一個窄圖,放在中間很不好看,干脆搞了和圖片主色相近的背景色。

## 提高效率
因為這個都是相對定位,所以場景里的元素的定位相比原型里1440 的小了一些,每個都得調一下。后來嘗試了chrome的workspace 方法 。直接瀏覽器里改,也沒用什么live reload方案。
### 彈窗
用了 Lulu UI的dialog。找了半天才找到回調在onClose里。