[TOC]
开始毫无头绪,找了几个专门做游戏的框架,layabox 装了打开报错、cocos studio 感觉太复杂,phaser感觉引用资源好复杂。
最后我感觉我这个小游戏没啥精灵动画,应该普通h5+js能实现。就拿了目前最熟悉可以双向绑定的vue来弄。
开发的时候感觉没效率,不知道游戏场景里放什么,后来干脆用墨刀设计了一个[原型](https://modao.cc/app/kyykUyNnY6dZUCWvfUAykOIfwhpkR2y)。
# UI设计
主要设计了欢迎页、游戏页、结束页 三个场景。
![欢迎页](https://box.kancloud.cn/9528337c572e8347c09ad455c6d2d639_773x753.png)
![游戏页](https://box.kancloud.cn/45c4884e747f274fd4f76c944d67e092_1123x806.png)
![结束页](https://box.kancloud.cn/1e8ad30804f22b3badbc897c7c183b0e_738x819.png)
# Vue的使用
## Vue 的生命周期
![生命周期](https://box.kancloud.cn/c7bfd289342941961506e8a3063bf621_1200x3039.png)
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实现的。
![](https://box.kancloud.cn/608ec2f53f76eac9be5c2e4fac8e00f7_1592x455.png)
于是就复制,去掉无用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>
~~~
## 结束页的背景
因为背景图是一个窄图,放在中间很不好看,干脆搞了和图片主色相近的背景色。
![](https://box.kancloud.cn/8a914636c84f5932cfdc681495ad7738_925x781.png)
## 提高效率
因为这个都是相对定位,所以场景里的元素的定位相比原型里1440 的小了一些,每个都得调一下。后来尝试了chrome的workspace 方法 。直接浏览器里改,也没用什么live reload方案。
### 弹窗
用了 Lulu UI的dialog。找了半天才找到回调在onClose里。