方格游戏的移动版适配开发,比我预计得快一点。
游戏界面预览
适配难点
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,//记量单位 移动端 移动的距离不再是固定的
isDictionaryUpdate:false,//字典中一部分数据只用更新一次,因此设置flag,isDictionaryUpdate 是否已经同步更新数据,默认否。
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;
if(this.isDictionaryUpdate){
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;
}
}else{
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;
}
this.isDictionaryUpdate=true;
}
},
//处理手机触摸事件
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;
}
}
}
};