方格游戏移动版
方格游戏的移动版适配开发,比我预计得快一点。
游戏界面预览
适配难点
1、touch 事件的四个方向的滑动监听。
解决:找到了一个大神写的工具类,完美解决问题。在此感谢。
2、方格宽高不固定,取值百分比,百分比浏览器返回值会舍弃小数点取整,导致对照字典偏移位置有偏差。
解决:设置计量单位,同步修改对照字典里的偏移量。计算分数时,由原来的全等于判断,变为比较偏差值。
3、touch 事件多次触发。
解决:设置布尔值 touchingScreen ,限制 touch 单次只能触发一次。
4、页面布局调整。
屏幕窗口有限,分数榜单和棋盘不再同时出现。也是因为宽度限制,棋盘方格使用百分比宽度,不再使用固定宽度。宽度百分比,设置高度等于宽度,需要 css 技巧。在方格上使用样式 touch-action: none; 这样任何触摸事件都不会产生默认行为,但是 touch 事件照样触发,从而解决无法被动侦听事件 preventDefault 。
undo
1、分数值的显示没有在方格里居中。
2、没有解决移动端浏览页面滚动的问题。
3、游戏开始时,小女孩正常移动后再回到起点会报一个错误。出错位置: this.chess[i].usedScore 报错信息: Cannot read property 'usedScore' of undefined.
主体代码 index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <title>方格游戏</title> <link rel="shortcut icon" href="favicon.ico"> <meta name="keywords" content="方格游戏"> <meta name="description" content="方格游戏"> <script src="vue.js"></script> <style> body{ width: 100%; height:100%; margin:0; background: #fff; overflow: hidden; box-sizing: border-box; color:#1a2a65; } div.container{ width: 100%; margin-top:80px; text-align: center; } div.container div.chess{ width: 98%; margin: 0 auto; box-sizing: border-box; border:1px solid #ccc; display: flex; justify-content: flex-start; flex-wrap: wrap; } div.chess-grid{ width:14.2857%; height:0; box-sizing: border-box; padding-bottom: 14.2857%; /* 让div的高等于宽 */ line-height: 100%; text-align: center; color: #fff; /*应用 CSS 属性 touch-action: none; 这样任何触摸事件都不会产生默认行为,但是 touch 事件照样触发,从而解决无法被动侦听事件preventDefault*/ touch-action: none; } /*雪人*/ div.snowman{ width:100%; padding-bottom: 100%; /* 让div的高等于宽 */ background: url("head.jpg") no-repeat; background-size: 100% 100%; cursor: pointer; } /*雪人的家*/ div.home{ width:100%; padding-bottom: 100%; /* 让div的高等于宽 */ background: url("home.jpg") no-repeat; background-size: 100% 100%; } div.container div.score{ width: 98%; margin: 0 auto; box-sizing: border-box; border:1px solid red; background: #f7f7f7; text-align: center; } div.container div.score .title{ font-size:18px; background:red; color:#fff; padding:10px; } /*游戏计时器*/ div.head{ width:100%; height:64px; box-sizing: border-box; text-align: center; position: fixed; top: 0; background: #f7f7f7; padding:12px; } div.head span,div.score span{ font-size: 24px; font-weight: bold; color:red; padding:0 4px } div.game_start_box{ display: flex; justify-content: center; } div.game_time_box{ margin-right:20px; line-height: 38px; } div.game_time_box input{ height:30px; width:40px; box-sizing: border-box; } div.game_start_btn{ width:100px; height:40px; line-height: 40px; box-sizing: border-box; border:1px solid #ccc; color: #fff; cursor: pointer; background: green; } /*控制器*/ div.control{ width:100%; height:60px; line-height:60px; text-align: center; display: flex; justify-content: center; position: fixed; bottom: 0; background: #f7f7f7; } div.btn{ width:200px; height:40px; line-height: 40px; box-sizing: border-box; border:1px solid #ccc; margin:10px; color: #fff; cursor: pointer; } div.auto{ background: green; } div.refresh{ background: red; } </style> </head> <body> <div id="root"> <div class="head" v-show="!showControl"> <div v-show="!showTime" class="game_start_box"> <div class="game_time_box">倒计时设置(10 ~ 60s):<input type="number" v-model="userSetGameTime" min="10" max="60"/></div> <div class="game_start_btn" @click="game_start">开始游戏</div> </div> <div v-show="showTime">倒计时<span>{{gameTime}}</span>秒 </div> </div> <div class="container"> <div class="chess" v-show="!showControl"> <div class="chess-grid"><div ref="snowman" class="snowman" :style="moveStyle" title="按键盘方向键移动我哦" @touchend="dealTouchEvent($event)"></div></div> <div class="chess-grid" v-for="(item,index) in chess" :key="index" :style="{background:item.background}">{{item.usedScore == 0 ? item.usedScore : item.score}}</div> <div class="chess-grid"><div class="home"></div></div> </div> <div class="score" v-show="showControl"> <div class="title">战绩</div> <p v-for="(score,index) in gameScores" :key="index">第{{index+1}}战得分<span>{{score}}</span>分</p> </div> </div> <div class="control" v-show="showControl"> <div class="btn auto" @click="game_review">本局复盘</div> <div class="btn refresh" @click="game_update">再来一局</div> </div> </div> <script src="touch.js"></script> <script> new Vue({ el: "#root", data: { touchingScreen:false,//是否正在触屏 限制 touch 触发频率 showControl:false,//是否显示本局复盘、再来一局 showTime:false,//是否显示倒计时 不显示倒计时的时候会显示开始游戏按钮 is_get_home:false, gameTime:30,//系统设置的游戏时间 30秒一局游戏 userSetGameTime:30,//用户设置的游戏时间 gameScores:[],//单机多人游戏 游戏复盘,存储历史得分 currentScore:0,//当前一局游戏的得分 moveDown:0, moveRitht:0, chess:[],//存储随机的分数和背景色数组 moveStyle:{transform:"translate(0,0)"},//偏移样式 unit:0,//记量单位 移动端 移动的距离不再是固定的 dictionary:[//棋盘各块偏移量对照字典 最后一格为终点格 {"score":0," i":0, "r":1, "d":0}, {"score":0, "i":1, "r":2, "d":0}, {"score":0,"i":2, "r":3, "d":0}, {"score":0,"i":3, "r":4, "d":0}, {"score":0,"i":4, "r":5, "d":0}, {"score":0,"i":5, "r":6, "d":0}, {"score":0,"i":6, "r":0, "d":1}, {"score":0,"i":7, "r":1, "d":1}, {"score":0,"i":8, "r":2, "d":1}, {"score":0,"i":9, "r":3, "d":1}, {"score":0,"i":10, "r":4, "d":1}, {"score":0,"i":11, "r":5, "d":1}, {"score":0,"i":12, "r":6, "d":1}, {"score":0,"i":13, "r":0, "d":2}, {"score":0,"i":14, "r":1, "d":2}, {"score":0,"i":15, "r":2, "d":2}, {"score":0,"i":16, "r":3, "d":2}, {"score":0,"i":17, "r":4, "d":2}, {"score":0,"i":18, "r":5, "d":2}, {"score":0,"i":19, "r":6, "d":2}, {"score":0,"i":20, "r":0, "d":3}, {"score":0,"i":21, "r":1, "d":3}, {"score":0,"i":22, "r":2, "d":3}, {"score":0,"i":23, "r":3, "d":3}, {"score":0,"i":24, "r":4, "d":3}, {"score":0,"i":25, "r":5, "d":3}, {"score":0,"i":26, "r":6, "d":3}, {"score":0,"i":27, "r":0, "d":4}, {"score":0,"i":28, "r":1, "d":4}, {"score":0,"i":29, "r":2, "d":4}, {"score":0,"i":30, "r":3, "d":4}, {"score":0,"i":31, "r":4, "d":4}, {"score":0,"i":32, "r":5, "d":4}, {"score":0,"i":33, "r":6, "d":4}, {"score":0,"i":34, "r":0, "d":5}, {"score":0,"i":35, "r":1, "d":5}, {"score":0,"i":36, "r":2, "d":5}, {"score":0,"i":37, "r":3, "d":5}, {"score":0,"i":38, "r":4, "d":5}, {"score":0,"i":39, "r":5, "d":5}, {"score":0,"i":40, "r":6, "d":5}, {"score":0,"i":41, "r":0, "d":6}, {"score":0,"i":42, "r":1, "d":6}, {"score":0,"i":43, "r":2, "d":6}, {"score":0,"i":44, "r":3, "d":6}, {"score":0,"i":45, "r":4, "d":6}, {"score":0,"i":46, "r":5, "d":6}, {"score":0,"i":47, "r":6, "d":6} ] }, methods: { //生成随机分数棋格 createChess:function(){ //7*7方格,掐头去尾,需要生成47个随机数。 var score; var bgColor; for(var i=0;i<47;i++){ // 按奇数偶数对应正负分值 if(i%2 ==0){ //正数 加分 score =Math.round(Math.random()*10)+2; bgColor="#16a05d"; }else{ //负数 减分 score =-Math.round(Math.random()*6)-1; bgColor="#e21918"; } this.chess.push({ "score":score, "usedScore":100,//随便指定一个现今规则不可能有的一个分数即可 "background":bgColor }); //同步更新对照字典,存储分值。 this.dictionary[i].score = score; //同步更新对照字典,计算准确坐标偏移量 this.dictionary[i].r = this.dictionary[i].r * this.unit; this.dictionary[i].d = this.dictionary[i].d * this.unit; } }, //处理手机触摸事件 dealTouchEvent:function(e){ e||event; this.touchingScreen = true; //使用的时候很简单,只需要像下面这样调用即可 up, right, down, left为四个回调函数,分别处理上下左右的滑动事件 EventUtil.listenTouchDirection(e.target,true,this.upCallback, this.rightCallback, this.downCallback, this.leftCallback); }, //touch的回调事件 upCallback:function(){ //当游戏倒计时显示时,即游戏还未结束,才能触发键盘事件,开始移动。 if(this.showTime && this.touchingScreen){ this.touchingScreen = false;//触屏只触发一次 //向上移动 this.moveDown -=this.unit; this.moveDown < 0 ? this.moveDown = 0 : this.moveDown; this.moveStyle.transform = "translate("+this.moveRitht+"px,"+this.moveDown+"px)"; //根据偏移的位置,统计得分。 this.countScore(this.moveRitht,this.moveDown); } }, rightCallback:function(){ if(this.showTime && this.touchingScreen){ this.touchingScreen = false;//触屏只触发一次 //向右移动 this.moveRitht +=this.unit; //判断界限值 不能超出棋盘活动 this.moveRitht > 6*this.unit ? this.moveRitht = 6*this.unit : this.moveRitht; this.moveStyle.transform = "translate("+this.moveRitht+"px,"+this.moveDown+"px)"; //根据偏移的位置,统计得分。 this.countScore(this.moveRitht,this.moveDown); } }, downCallback:function(){ if(this.showTime && this.touchingScreen){ this.touchingScreen = false;//触屏只触发一次 //向下移动 this.moveDown +=this.unit; this.moveDown > 6*this.unit ? this.moveDown = 6*this.unit : this.moveDown; this.moveStyle.transform = "translate("+this.moveRitht+"px,"+this.moveDown+"px)"; //根据偏移的位置,统计得分。 this.countScore(this.moveRitht,this.moveDown); } }, leftCallback:function(){ if(this.showTime && this.touchingScreen){ this.touchingScreen = false;//触屏只触发一次 //向左移动 this.moveRitht -=this.unit; //判断界限值 不能超出棋盘活动 this.moveRitht < 0 ? this.moveRitht = 0 : this.moveRitht; this.moveStyle.transform = "translate("+this.moveRitht+"px,"+this.moveDown+"px)"; //根据偏移的位置,统计得分。 this.countScore(this.moveRitht,this.moveDown); } }, //计算得分 countScore:function(r,d){ //遍历偏移量字典,根据当前所在的位置,获取对应的分值。 //偏移量字典(len=48)比棋格(len=47)多了一个终点的位置信息。 if(!(Math.abs(r - this.unit*6) < 7 && Math.abs(d - this.unit*6) < 7)){ //不在家,赋值false 防止回家后再离开的情形 this.is_get_home = false; for(var i=0;i<48;i++){ //因为浏览器将数值取整后返回,所以,与实际值差值小于1.累积偏移差值小于1*7. Math.abs 取绝对值. if(Math.abs(r - this.dictionary[i].r) < 7 && Math.abs(d - this.dictionary[i].d) < 7){ //undo: this.chess[i].usedScore 有时会报错 Cannot read property 'usedScore' of undefined if(this.chess[i].usedScore == 100){ this.currentScore += this.dictionary[i].score; //分数一次性有效 走过的分数变为0. //为了复盘,不直接改变分数,新分数存储到 usedScore this.chess[i].usedScore = 0; } } } }else{ //雪人到家 this.is_get_home = true; } }, //计时器,每次时间-1,时间单位秒。 timer:function(){ this.gameTime -= 1 }, //用户点击游戏开始 创建定时器 显示倒计时 game_start:function(){ if(this.gameTime != this.userSetGameTime){ //系统设置的游戏时长和用户设置的游戏时长冲突,则使用用户设置的时长 this.gameTime = this.userSetGameTime; } this.showTime=true; timer1=setInterval(this.timer, 1000); }, game_review:function(){ this.parameter_reset(); this.resetUsedScore(); }, //恢复棋盘 使用过的分数初始化 resetUsedScore:function(){ var len = 47; while(len--){ this.chess[len].usedScore = 100; } }, //本局重玩,只需要重置参数。 parameter_reset:function(){ this.showControl=false; this.is_get_home = false; this.showTime=false;//先不显示倒计时,显示开始游戏按钮。 this.moveRitht=0; this.moveDown=0; this.moveStyle.transform = "translate(0,0)"; this.currentScore=0; }, //页面初始化 游戏重新开始 game_update:function(){ this.parameter_reset(); this.gameScores=[]; this.chess=[]; this.createChess(); } }, watch:{//监测游戏时间 gameTime(){ if(this.gameTime == 0){ clearInterval(timer1); this.showControl=true; this.showTime=false;//倒计时结束,关闭倒计时结果显示 if(this.is_get_home){ //游戏结束:倒计时结束,雪人进入小屋。当前得分计入。 this.gameScores.push(this.currentScore); }else{ //游戏失败: 倒计时结束,但雪人未进入小屋。本局得分为0。 this.currentScore=0; this.gameScores.push(0); } } } }, mounted(){ //计量单位等于小方格的宽或高 //获取的高度值约等于实际值,存在差值。获取的值取了实际值的近似整数。 this.unit = this.$refs.snowman.offsetHeight; this.game_update(); } }) </script> </body> </html>
引用的大神代码工具类 touch.js
var EventUtil = { addHandler: function (element, type, handler) { if (element.addEventListener) element.addEventListener(type, handler, false); else if (element.attachEvent) element.attachEvent("on" + type, handler); else element["on" + type] = handler; }, removeHandler: function (element, type, handler) { if(element.removeEventListener) element.removeEventListener(type, handler, false); else if(element.detachEvent) element.detachEvent("on" + type, handler); else element["on" + type] = handler; }, /** * 监听触摸的方向 * @param target 要绑定监听的目标元素 * @param isPreventDefault 是否屏蔽掉触摸滑动的默认行为(例如页面的上下滚动,缩放等) * @param upCallback 向上滑动的监听回调(若不关心,可以不传,或传false) * @param rightCallback 向右滑动的监听回调(若不关心,可以不传,或传false) * @param downCallback 向下滑动的监听回调(若不关心,可以不传,或传false) * @param leftCallback 向左滑动的监听回调(若不关心,可以不传,或传false) */ listenTouchDirection: function (target, isPreventDefault, upCallback, rightCallback, downCallback, leftCallback) { this.addHandler(target, "touchstart", handleTouchEvent); this.addHandler(target, "touchend", handleTouchEvent); this.addHandler(target, "touchmove", handleTouchEvent); var startX; var startY; function handleTouchEvent(event) { switch (event.type){ case "touchstart": startX = event.touches[0].pageX; startY = event.touches[0].pageY; break; case "touchend": var spanX = event.changedTouches[0].pageX - startX; var spanY = event.changedTouches[0].pageY - startY; if(Math.abs(spanX) > Math.abs(spanY)){ //认定为水平方向滑动 if(spanX > 30){ //向右 if(rightCallback) rightCallback(); } else if(spanX < -30){ //向左 if(leftCallback) leftCallback(); } } else { //认定为垂直方向滑动 if(spanY > 30){ //向下 if(downCallback) downCallback(); } else if (spanY < -30) {//向上 if(upCallback) upCallback(); } } break; case "touchmove": //阻止默认行为 if(isPreventDefault) event.preventDefault(); break; } } } };