推箱子游戏是老游戏了, 网上有各种各样的版本, 说下推箱子游戏的简单实现,以及我找到的一些参考视频和实例;
如下是效果图:
这个拖箱子游戏做了移动端的适配, 我使用了zepto的touch模块, 通过手指滑动屏幕就可以控制乌龟走不同的方向;
因为推箱子这个游戏比较简单, 直接用了过程式的方式写代码, 模块也就是两个View 和 Model, 剩下就是用户的事件Controller, 用户每一次按下键盘的方向键都会改变数据模型的数据,然后重新生成游戏的静态html, 然后用innerHTML方式插入到界面, 自动生成DOM节点;
游戏的关卡模型就是数据, 我把每一关的数据分为三块:
地图数据,二维数组(地图数据包括板砖, 箱子要去的目标位置, 空白的位置)
箱子数据,一维数组(箱子的初始位置)
小乌龟的数据,json对象
每一个关卡都有对应的游戏关卡数据, 模拟的数据如下:
level: [ { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,0,1,1,1,0,0,0,0], [0,1,1,3,3,1,0,0,0], [0,1,0,0,0,0,1,0,0], [0,1,0,0,0,0,1,0,0], [0,1,1,1,1,1,1,0,0] ], person: {x : 2, y : 2}, box: [{x:3, y : 2},{x:4,y:2}] }, //第二关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,1,1,1,1,1,0,0], [0,1,0,0,1,1,1,0], [0,1,0,0,0,0,1,0], [1,1,1,0,1,0,1,1], [1,3,1,0,1,0,0,1], [1,3,0,0,0,1,0,1], [1,3,0,0,0,0,0,1], [1,1,1,1,1,1,1,1] ], person: {x : 2, y : 2}, box: [{x:3, y : 2}, {x:2,y:5} ,{x:5, y:6}] /* box : [ {x:3, y : 1}, {x:4, y : 1}, {x:4, y : 2}, {x:5, y : 5} ] */ }, //第三关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,0,0,1,1,1,1,1,1,0], [0,1,1,1,0,0,0,0,1,0], [1,1,3,0,0,1,1,0,1,1], [1,3,3,0,0,0,0,0,0,1], [1,3,3,0,0,0,0,0,1,1], [1,1,1,1,1,1,0,0,1,0], [0,0,0,0,0,1,1,1,1,0] ], person: {x : 8, y : 3}, box: [{x:4, y : 2}, {x:3,y:3} ,{x:4, y:4},{x:5, y:3},{x:6, y:4}] }, //第四关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,1,1,1,1,1,1,1,0,0], [0,1,0,0,0,0,0,1,1,1], [1,1,0,1,1,1,0,0,0,1], [1,0,0,0,0,0,0,0,0,1], [1,0,3,3,1,0,0,0,1,1], [1,1,3,3,1,0,0,0,1,0], [0,1,1,1,1,1,1,1,1,0] ], person: {x : 2, y : 3}, box: [{x:2, y : 2}, {x:4,y:3} ,{x:6, y:4},{x:7, y:3},{x:6, y:4}] }, //第五关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,0,1,1,1,1,0,0], [0,0,1,3,3,1,0,0], [0,1,1,0,3,1,1,0], [0,1,0,0,0,3,1,0], [1,1,0,0,0,0,1,1], [1,0,0,1,0,0,0,1], [1,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1] ], person: {x : 4, y : 6}, box: [{x:4, y : 3}, {x:3,y:4} ,{x:4, y:5}, {x:5,y:5}] /* box : [ {x:3, y : 1}, {x:4, y : 1}, {x:4, y : 2}, {x:5, y : 5} ] */ }, //第六关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,0,0,0,1,1,1,1,1,1,1,0], [0,0,0,0,1,0,0,1,0,0,1,0], [0,0,0,0,1,0,0,0,0,0,1,0], [1,1,1,1,1,0,0,1,0,0,1,0], [3,3,3,1,1,0,0,0,0,0,1,1], [3,0,0,1,0,0,0,0,1,0,0,1], [3,0,0,0,0,0,0,0,0,0,0,1], [3,0,0,1,0,0,0,0,1,0,0,1], [3,3,3,1,1,1,0,1,0,0,1,1], [1,1,1,1,1,0,0,0,0,0,1,0], [0,0,0,0,1,0,0,1,0,0,1,0], [0,0,0,0,1,1,1,1,1,1,1,0] ], person: {x : 5, y : 10}, box: [ {x:5, y:6}, {x:6, y:3}, {x:6, y:5}, {x:6, y:7}, {x:6, y:9}, {x:7, y:2}, {x:8, y:2}, {x:9, y:6} ] } ]
有一个很重要的东西就是推箱子游戏的主要逻辑:因为小乌龟走的地方只能是空白的区域,而且乌龟前面有墙就不能走, 或者乌龟前面是箱子,就再判断箱子前面是否有墙, 如果没有墙乌龟和箱子都可以走往前走一步,如果有墙就不能走。每一次小乌龟走了都改变地图数据,然后重新生成界面,如此循环, 每一小乌龟走完都要检测地图数据中的箱子数据是否全对上了,对上了就给用户提示, 并进入下一关;
游戏的模板引擎用了handlebarsJS, 可以去官网看API 。 这个是写过的一篇博客,Handlebars的使用方法文档整理(Handlebars.js):打开, 模板内容:
<script id="tpl" type="text/x-handlebars-template"> {{#initY}}{{/initY}} {{#each this}} {{#each this}} <div class="{{#getClass this}}{{/getClass}}" data-x="{{@index}}" data-y="{{#getY}}{{/getY}}" style="left:{{#calc @index}}{{/calc}};top:{{#calc 1111}}{{/calc}}"> <!--{{@index}} {{#getY}}{{/getY}} --> </div> {{/each}} {{#addY}}{{/addY}} {{/each}} </script>
为Handlebars定了几个helper,包括initY, getClass, getY,calc 、、、、,模板引擎主要是辅助的作用, 这边用Handlebars不是很明智啊, 代码的可读性变差了点, 这里面也利用了闭包保存变量, 避免全局变量的污染:
(function() { var y = 0; Handlebars.registerHelper("initY", function() { y = 0; }); Handlebars.registerHelper("addY", function() { y++; }); Handlebars.registerHelper("getY", function() { return y; }); Handlebars.registerHelper("calc", function(arg) { //console.log(arg) if(arg!==1111) { return 50*arg + "px"; }else{ return 50*y + "px"; }; }); Handlebars.registerHelper("getClass", function(arg) { switch( arg ) { case 0 : return "bg" case 1 : return "block" case 2 : return "box" case 3 : return "target" }; }); window.util = { isMobile : function() { return navigator.userAgent.toLowerCase().indexOf("mobile") !== -1 || navigator.userAgent.toLowerCase().indexOf("android") !== -1 || navigator.userAgent.toLowerCase().indexOf("pad") !== -1; } } })();
因为要兼容移动端, 我们要检查是否是手机或者平板,如果是的话,我就添加对应的DOM元素(方向键DOM元素),然后绑定对应的事件, zeptoJS提供了touch模块,我们要去官网去找,然后额外引用进来,打开地址 , 然后就可以使用swipeLeft,swipeUp,swipeDown, swipeRight 这几个事件:
if( window.util.isMobile() ) { $(window).on("swipeLeft",function() { _this.step("left"); }).on("swipeRight",function() { _this.step("right"); }).on("swipeUp",function() { _this.step("top"); }).on("swipeDown",function() { _this.step("bottom"); }); mobileDOM(); $(".arrow-up").tap(function() { _this.step("top"); }); $(".arrow-down").tap(function() { _this.step("bottom"); }); $(".arrow-left").tap(function() { _this.step("left"); }); $(".arrow-right").tap(function() { _this.step("right"); }); }else{ $(window).on("keydown", function(ev) { var state = ""; switch( ev.keyCode ) { case 37 : state = "left"; break; case 39 : state = "right"; break; case 38 : state = "top"; break; case 40 : state = "bottom"; break; }; _this.step(state) }); };
因为要保存用户的当前关卡, 也额外引用了jQuery-cookies插件, 每一次闯关成功,我们就保存一次当前的闯关记录, 当用户不想玩或者别的原因关闭了浏览器, 过几天想重新玩的时候可以继续玩;
if( G.now+1 > G.level.length-1 ) { alert("闯关成功"); return ; }else{ //如果可用的等级大于当前的等级,就把level设置进去; if( G.now+1 > parseInt( $.cookie('level') || 0 )) { $.cookie('level' , G.now+1 , { expires: 7 }); }; start( G.now+1 ); return ; };
所有的代码在这里:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.4/css/bootstrap.min.css"> <link rel="stylesheet" href="http://sqqihao.github.io/games/rusBlock/libs/Tiny-Alert/css/zepto.alert.css"/> <script src="libs/jquery-1.9.1.min.js"></script> <script src="libs/handlebars.js"></script> <script src="libs/jquery-cookie.js"></script> <script src="http://sqqihao.github.io/games/rusBlock/libs/Tiny-Alert/js/zepto.alert.js"></script> <script id="tpl" type="text/x-handlebars-template"> {{#initY}}{{/initY}} {{#each this}} {{#each this}} <div class="{{#getClass this}}{{/getClass}}" data-x="{{@index}}" data-y="{{#getY}}{{/getY}}" style="left:{{#calc @index}}{{/calc}};top:{{#calc 1111}}{{/calc}}"> <!--{{@index}} {{#getY}}{{/getY}} --> </div> {{/each}} {{#addY}}{{/addY}} {{/each}} </script> <script> (function() { var y = 0; Handlebars.registerHelper("initY", function() { y = 0; }); Handlebars.registerHelper("addY", function() { y++; }); Handlebars.registerHelper("getY", function() { return y; }); Handlebars.registerHelper("calc", function(arg) { //console.log(arg) if(arg!==1111) { return 50*arg + "px"; }else{ return 50*y + "px"; }; }); Handlebars.registerHelper("getClass", function(arg) { switch( arg ) { case 0 : return "bg" case 1 : return "block" case 2 : return "box" case 3 : return "target" }; }); window.util = { isMobile : function() { return navigator.userAgent.toLowerCase().indexOf("mobile") !== -1 || navigator.userAgent.toLowerCase().indexOf("android") !== -1 || navigator.userAgent.toLowerCase().indexOf("pad") !== -1; } } })(); </script> </head> <style> #game{ display: none; } #house{ position: relative; } .bg{ position: absolute; width:50px; height:50px; box-sizing: border-box; } .block{ position: absolute; background-image: url(imgs/wall.png); width:50px; height:50px; box-sizing: border-box; } .box{ position: absolute; background: #fbd500; width:50px; height:50px; background-image: url(imgs/box.png); } .target{ position: absolute; background: url(imgs/target.jpg); background-size: 50px 50px;; width:50px; height:50px; box-sizing: border-box; } #person{ background-image: url(imgs/person.png); width:50px; height:50px; position: absolute; } #person.up{ background-position: 0 0; } #person.right{ background-position:-50px 0 ; } #person.bottom{ background-position:-100px 0 ; } #person.left{ background-position:-150px 0 ; } /*移动端的DOM*/ .operate-bar{ font-size:30px; } .height20percent{ height:30%; } .height30percent{ height:30%; } .height40percent{ height:40%; } .height100percent{ height:100%; } .font30{ font-size:30px; color:#34495e; } </style> <body> <div id="select"> <div class="container"> <div class="row"> <p class="text-info"> 已经解锁的关卡: <p id="level"> </p> </p> <button id="start" class="btn btn-default"> 开始游戏 </button> </div> </div> </div> <div id="game" class="container"> <div class="row"> <button onclick="location.reload()" class="btn btn-info" > 返回选择关卡重新 </button> <div id="house"> </div> </div> </div> <script> G = { level: [ { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,0,1,1,1,0,0,0,0], [0,1,1,3,3,1,0,0,0], [0,1,0,0,0,0,1,0,0], [0,1,0,0,0,0,1,0,0], [0,1,1,1,1,1,1,0,0] ], person: {x : 2, y : 2}, box: [{x:3, y : 2},{x:4,y:2}] }, //第二关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,1,1,1,1,1,0,0], [0,1,0,0,1,1,1,0], [0,1,0,0,0,0,1,0], [1,1,1,0,1,0,1,1], [1,3,1,0,1,0,0,1], [1,3,0,0,0,1,0,1], [1,3,0,0,0,0,0,1], [1,1,1,1,1,1,1,1] ], person: {x : 2, y : 2}, box: [{x:3, y : 2}, {x:2,y:5} ,{x:5, y:6}] /* box : [ {x:3, y : 1}, {x:4, y : 1}, {x:4, y : 2}, {x:5, y : 5} ] */ }, //第三关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,0,0,1,1,1,1,1,1,0], [0,1,1,1,0,0,0,0,1,0], [1,1,3,0,0,1,1,0,1,1], [1,3,3,0,0,0,0,0,0,1], [1,3,3,0,0,0,0,0,1,1], [1,1,1,1,1,1,0,0,1,0], [0,0,0,0,0,1,1,1,1,0] ], person: {x : 8, y : 3}, box: [{x:4, y : 2}, {x:3,y:3} ,{x:4, y:4},{x:5, y:3},{x:6, y:4}] }, //第四关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,1,1,1,1,1,1,1,0,0], [0,1,0,0,0,0,0,1,1,1], [1,1,0,1,1,1,0,0,0,1], [1,0,0,0,0,0,0,0,0,1], [1,0,3,3,1,0,0,0,1,1], [1,1,3,3,1,0,0,0,1,0], [0,1,1,1,1,1,1,1,1,0] ], person: {x : 2, y : 3}, box: [{x:2, y : 2}, {x:4,y:3} ,{x:6, y:4},{x:7, y:3},{x:6, y:4}] }, //第五关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,0,1,1,1,1,0,0], [0,0,1,3,3,1,0,0], [0,1,1,0,3,1,1,0], [0,1,0,0,0,3,1,0], [1,1,0,0,0,0,1,1], [1,0,0,1,0,0,0,1], [1,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1] ], person: {x : 4, y : 6}, box: [{x:4, y : 3}, {x:3,y:4} ,{x:4, y:5}, {x:5,y:5}] /* box : [ {x:3, y : 1}, {x:4, y : 1}, {x:4, y : 2}, {x:5, y : 5} ] */ }, //第六关 { //0是空的地图 //1是板砖 //3是目标点 state:[ [0,0,0,0,1,1,1,1,1,1,1,0], [0,0,0,0,1,0,0,1,0,0,1,0], [0,0,0,0,1,0,0,0,0,0,1,0], [1,1,1,1,1,0,0,1,0,0,1,0], [3,3,3,1,1,0,0,0,0,0,1,1], [3,0,0,1,0,0,0,0,1,0,0,1], [3,0,0,0,0,0,0,0,0,0,0,1], [3,0,0,1,0,0,0,0,1,0,0,1], [3,3,3,1,1,1,0,1,0,0,1,1], [1,1,1,1,1,0,0,0,0,0,1,0], [0,0,0,0,1,0,0,1,0,0,1,0], [0,0,0,0,1,1,1,1,1,1,1,0] ], person: {x : 5, y : 10}, box: [ {x:5, y:6}, {x:6, y:3}, {x:6, y:5}, {x:6, y:7}, {x:6, y:9}, {x:7, y:2}, {x:8, y:2}, {x:9, y:6} ] } ], //map data mapData : (function() { var data = {}; return { get: function () { return data; }, set: function (arg) { data = arg; }, //穿进来的数据在界面中是否存在; collision: function (x, y) { if( data.state[y][x] === 1)return true; return false; }, collisionBox : function(x,y) { for(var i= 0, len= data.box.length; i< len; i++) { if( data.box[i].x === x&& data.box[i].y === y)return data.box[i]; }; return false; } } })(), view : { initMap : function(map) { document.getElementById("house").innerHTML = Handlebars.compile( document.getElementById("tpl").innerHTML )( map ); }, initPerson : function(personXY) { var per = document.createElement("div"); per.id = "person"; G.per = per; document.getElementById("house").appendChild(per); per.style.left = 50* personXY.x+"px"; per.style.top = 50* personXY.y+"px"; }, initBox : function(boxs) { for(var i=0;i<boxs.length; i++) { var box = document.createElement("div"); box.className = "box"; G.box = box; document.getElementById("house").appendChild(box); box.style.left = boxs[i].x*50 + "px"; box.style.top = boxs[i].y*50 + "px"; }; }, deleteBox : function() { var eBoxs = document.getElementsByClassName("box"); var len = eBoxs.length; while( len-- ) { eBoxs[len].parentNode.removeChild( eBoxs[len] ); }; } }, /* * 0;向上 * 1:向右 * 2:向下 * 3:向左 * */ direction : 0, step : function(xy) { //这里面要做很多判断 /*包括: 用户当前的方向和以前是否一样,如果不一样要先转头; 如果一样的话,判断前面是否有石头, 是否有箱子; 如果前面有墙壁或者 前面有箱子,而且箱子前面有墙壁就return 把人物往前移动 如果人物的位置上有一个箱子,把箱子也移动一下; */ var mapData = this.mapData.get(); //对参数进行处理; if ( typeof xy === "string" ) { var x = 0, y = 0, xx = 0, yy = 0; switch( xy ) { case "left" : if(this.direction==0){ x = -1; xx = -2; }else{ x = 0; }; this.direction = 0; break; case "top" : if(this.direction===1){ y = -1; yy = -2 }else{ y = 0; }; this.direction = 1; break; case "right" : if(this.direction === 2) { x = 1; xx = 2; }else{ x = 0; }; this.direction = 2; break; case "bottom" : if(this.direction ===3 ) { y = 1; yy = 2; }else{ y = 0; }; this.direction = 3; }; //如果是墙壁就不能走 if( this.mapData.collision(mapData.person.x + x, mapData.person.y+y) ) { return; }; //如果碰到的是箱子, 而且箱子前面是墙壁, 就return if( this.mapData.collisionBox(mapData.person.x+x, mapData.person.y+y) && this.mapData.collision(mapData.person.x+xx, mapData.person.y+yy)) { return; }; if( this.mapData.collisionBox(mapData.person.x+x, mapData.person.y+y) && this.mapData.collisionBox(mapData.person.x+xx, mapData.person.y+yy)) { return } //mapData.x+xx, mapData.y+yy mapData.person.x = mapData.person.x + x; mapData.person.y = mapData.person.y + y; this.per.style.left = 50* mapData.person.x+"px"; this.per.style.top = 50* mapData.person.y+"px"; this.per.className = { 0:"up", 1:"right", 2:"bottom", 3:"left" }[this.direction]; var theBox = {}; if(theBox = this.mapData.collisionBox(mapData.person.x, mapData.person.y)) { theBox.x = mapData.person.x+x; theBox.y = mapData.person.y+y; this.view.deleteBox(); this.view.initBox(mapData.box); this.testSuccess(); }; //如果碰到了箱子,而且箱子前面不能走就return, 否则就走箱子和人物; }; }, /* * return Boolean; * */ //遍历所有的box,如果在box中的所有x,y在地图中对应的值为3,全部通过就返回true testSuccess : function() { var mapData = this.mapData.get(); for(var i=0; i<mapData.box.length; i++) { if(mapData.state[mapData.box[i].y][mapData.box[i].x] != 3) { return false; }; }; $.dialog({ content : '游戏成功, 进入下一关!', title : 'alert', ok : function() { if( G.now+1 > G.level.length-1 ) { alert("闯关成功"); return ; }else{ //如果可用的等级大于当前的等级,就把level设置进去; if( G.now+1 > parseInt( $.cookie('level') || 0 )) { $.cookie('level' , G.now+1 , { expires: 7 }); }; start( G.now+1 ); return ; }; }, cancel : function(){ location.reload(); }, lock : true }); }, //这里面需要处理 map, 人物数据, box数据 init : function() { //更新地图; //this.level[0].state this.view.initMap( this.mapData.get().state ); this.view.initPerson( this.mapData.get().person ); this.view.initBox( this.mapData.get().box ); //this.person = this.factory.Person(0,0); //this.box = this.factory.Box([{x:0,y:1},{x:1,y:1},{x:0,y:2},{x:1,y:2}]); if( this.hasBind ) { return }; this.hasBind = true; this.controller(); }, controller : function() { function mobileDOM() { var mobileDOMString = '\ <div class="navbar-fixed-bottom height20percent operate-bar" >\ <div class="container height100percent">\ <div class="row text-center height100percent">\ <div class="height40percent arrow-up">\ <span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span>\ </div>\ <div class="height30percent">\ <div class="col-xs-6 arrow-left">\ <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>\ </div>\ <div class="col-xs-6 arrow-right">\ <span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span>\ </div>\ </div>\ <div class="height30percent arrow-down">\ <span class="glyphicon glyphicon-arrow-down" aria-hidden="true"></span>\ </div>\ </div>\ </div>\ </div>\ '; +function addDOM() { $("#game").append( mobileDOMString ); }(); }; var _this = this; if( window.util.isMobile() ) { $(window).on("swipeLeft",function() { _this.step("left"); }).on("swipeRight",function() { _this.step("right"); }).on("swipeUp",function() { _this.step("top"); }).on("swipeDown",function() { _this.step("bottom"); }); mobileDOM(); $(".arrow-up").tap(function() { _this.step("top"); }); $(".arrow-down").tap(function() { _this.step("bottom"); }); $(".arrow-left").tap(function() { _this.step("left"); }); $(".arrow-right").tap(function() { _this.step("right"); }); }else{ $(window).on("keydown", function(ev) { var state = ""; switch( ev.keyCode ) { case 37 : state = "left"; break; case 39 : state = "right"; break; case 38 : state = "top"; break; case 40 : state = "bottom"; break; }; _this.step(state) }); }; } }; function start( level ) { G.now = level; G.mapData.set(G.level[level] ); G.init(); $("#game").show(); $("#select").hide(); }; function init() { var cookieLevel = $.cookie('level') || 0; start( cookieLevel ); }; $("#start").click(function() { init(); }); String.prototype.repeat = String.prototype.repeat || function(num) { return (new Array(num+1)).join( this.toString() ); }; window.onload = function() { var cookieLevel = $.cookie('level') || 0; $("#level").html( function() { var index = 0; return "<a href='###' class='btn btn-info' onclick='start({{i}})'>关卡</a> ".repeat((parseInt($.cookie('level')) || 0)+1).replace(/{{i}}/gi, function() { return index++; }) }); } </script> </body> </html>
游戏一共有6关, 每一关成功通过即可解锁下一关, 地图的话其实可以多找些的,哈哈;
推箱子游戏的在线DEMO : 打开
以上所述就是本文的全部内容了,希望大家能够喜欢。
JavaScript编写推箱子游戏
- Author -
hebedich声明:登载此文出于传递更多信息之目的,并不意味着赞同其观点或证实其描述。
Reply on: @reply_date@
@reply_contents@