ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[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 !=''">&nbsp;通知:{{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里。