写点什么

HTML5 游戏开发实战 | 推箱子

作者:TiAmo
  • 2023-06-28
    江苏
  • 本文字数:7269 字

    阅读完需:约 24 分钟

HTML5 游戏开发实战 | 推箱子

经典的推箱子是一个来自日本的古老游戏,目的是在训练玩家的逻辑思考能力。在一个狭小的仓库中,要求把木箱放到指定的位置,稍不小心就会出现箱子无法移动或者通道被堵住的情况,所以需要巧妙地利用有限的空间和通道,合理安排移动的次序和位置,才能顺利地完成任务!


经典的推箱子是一个来自日本的古老游戏,目的是在训练玩家的逻辑思考能力。在一个狭小的仓库中,要求把木箱放到指定的位置,稍不小心就会出现箱子无法移动或者通道被堵住的情况,所以需要巧妙地利用有限的空间和通道,合理安排移动的次序和位置,才能顺利地完成任务。

推箱子游戏功能如下:

游戏运行载入相应的地图,屏幕中出现一个推箱子的工人,其周围是围墙、人可以走的通道、几个可以移动的箱子和箱子放置的目的地。让玩家通过按上、下、左、右键控制工人推箱子,当箱子都推到了目的地后出现过关信息,并显示下一关。推错了玩家可以撤销移动或者重新玩这关,直到通过全部关卡。

推箱子游戏的运行界面如上图所示。

本游戏使用的图片元素的含义如图 9-2 所示。


01、箱子游戏设计的思路

先来确定一下开发难点。对工人的操作很简单,就是 4 个方向移动。注意在工人移动时箱子也移动,此效果对按键处理的要求也比较简单。当箱子到达目的地位置时,需会产生游戏过关事件,需要一个逻辑判断。那么仔细想一下,这些所有的事件都发生在一张地图中。这张地图包括了箱子的初始化位置、箱子最终放置的位置,以及围墙障碍等。每一关地图都要更换,这些位置也要变。所以每一关的地图数据是最关键的,它决定了每一关的不同场景和物体位置。那么下面就重点分析一下地图。

假设把地图想象成一个网格,每个格子就是工人每次移动的步长,也是箱子移动的距离,这样问题就简化多了。首先设计一个 16×16 的二维数组 curMap。按照这样的框架来思考。对于格子的 X,Y 两个屏幕像素坐标,可以由二维列表下标换算。

每个格子状态值分别用值(0)代表通道 Block,(1)代表墙 Wall,(2)代表目的地 Ball,(3)代表箱子 Box,(4)代表工人 CurMan,(5)代表放到目的地的箱子 redBox。文件中存储的原始地图中格子的状态值采用相应的整数形式存放。

在玩家通过键盘控制工人推箱子的过程中,需要按游戏规则进行判断是否响应该按键指示。下面分析一下工人将会遇到什么情况,以便归纳出所有的规则和对应算法。为了描述方便,可以假设工人移动趋势方向为向右,其他方向原理是一致的。如图 9-4 所示,P1、P2 分别代表工人移动趋势方向的前两个方格。


■ 图 9-4 工人移动趋势(向右)

游戏规则判断如下。

(1) 判断 P1 是否出界,出界则退出规则判断,布局不做任何改变。

if(p1.x< 0) return false;if(pl.y< 0) return false;if(pl.y>= curMap.length)return false;if(p1.x>= curMap[0].length)return false;
复制代码

(2) 前方 P1 是围墙。

如果工人前方是围墙(即阻挡工人的路线)
{退出规则判断,布局不做任何改变;
}
if(curMap[p1.y][p1.x] == 1return false; //如果是墙,不能通行
复制代码

如果工人前方是围墙(即阻挡工人的路线)

{退出规则判断,布局不做任何改变;

}

if(curMap[p1.y][p1.x] == 1return false; //如果是墙,不能通行(3) 前方 P1 是箱子,如图 9-5 所示。


■ 图 9-5 工人前方是箱子

(3) 前方 P1 是箱子,如图 9-5 所示。

在前面的情况中,只要根据前方 P1 处的物体就可以判断出工人是否可以移动,而在第 3 种情况中,需要判断箱子前方 P2 处的物体才能判断出工人是否可以移动。此时有以下几种可能。

① P1 处为箱子或者放到目的地的箱子,P2 处为墙或箱子。

如果工人前方 P1 处为箱子或者放到目的地的箱子,P2 处为墙或箱子,退出规则判断,布局不做任何改变。

if(curMap[pl.y][p1.x]== 3 curMap[p1.y][p1.x]== 5)//如果是箱子,继续判断前一格if(curMap[p2.y][p2.x] == 1  curMap[ p2.y][p2.x]== 3curMap[p2.y][p2.x]== 5)return false;//前一格如果是墙或箱子,则不能前进
复制代码

② P1 处为箱子或者放到目的地的箱子,P2 处为通道。

如果工人前方 P1 处为箱子,P2 处为通道,工人可以进到 P1 方格,P2 方格状态为箱子。修改相关位置格子的状态值。

③ P1 处为箱子或者放到目的地的箱子,P2 处为目的地。

如果工人前方 P1 处为箱子,P2 处为目的地,工人可以进到 P1 方格,P2 方格状态为放置好的箱子。修改相关位置格子的状态值。

//如果是箱子,继续判断前一格if(curMap[pl.y][p1.x]== 3 curMap[p1.y][p1.x]== 5)if(curMap[p2.y][p2.x]==0 curMap[p2.y][p2.x]== 2) //如果 P2 为通道或者目的地//记录现在的地图oldMap = copyArray(curMap);//箱子前进一格curMap[p2.y][ p2.x]= 3;//如果原始地图是目的地或者是放到目的地的箱子if(CurLevel[p2.y][p2.x] == 2 CurLevel[p2.y][p2.x] == 5)curMap[p2.y][p2.x] = 5;canReDo = true;//工人前进一格curMap[ p1.y][p1.x] = 4; //4 代表工人//处理工人原来位置是显示目的地还是通道平地//获取工人原来位置原始地图信息var v= CurLevel[per position.y][per position.x];if(v== 2 v== 5)[ //如果原来位置是目的地或者放到目的地的箱子curMap[per_position.y][per position.x]=2;//显示目的地else//显示通道平地curMap[per position.y][per position.x]=0;
复制代码

综合前面的分析,可以设计出整个游戏的实现流程。

02、推箱子游戏设计的步骤

游戏页面 pushbox.html

<htmI >< head ><title>推箱子游戏</title>< meta http - equiv = content - type content = "text/html; charset = utf - 8"></head ><body onload ="init()”onkeydown ="DoKeyDown(event)"><canvas id="myCanvas”width="560"heght ="560">浏览器还不支持哦</canvas><div id="msg"></div><img id="block”src = "img/block.gif”style = "display:none;">< img id ="wall"src ="img/wall.gif"style = "display:none;"><img id="ball"src ="img/ball.gif”style="display:none;1< img id="box”src ="img/box.gif"style ="display:none;">< img id= "redbox”src ="img/redbox.gif"style ="display:none;">< img id="pleft"src = "img/left.png"style ="display:none;'<img id="pright”src = "img/right.png"style="display:none;"><img id="pup”src ="img/up.png”style="display:none;">< img id="pdown”src ="img/down.pngstyle ="display:none;"><input type ="button"value ="上一关"onclick ="NextLevel( - 1)">< input type ="button"value ="下一关"onclick ="NextLevel(1)">< input type="button"value ="撤销移动”onclick ="Redo()"><input type="button"value ="重玩本关”onclick ="NextLevel(0)">< input type="button"value ="游戏说明”onclick ="DoHelp()">< script type = "text/javascript"src = "mapdata100.js"></script><script type ="text/javascript"src = "pushboxl.js"></script></body></html>
复制代码

游戏页面主要设置图片素材对应的 id 。例如,箱子图片的 id 是“box" ,目的地图片的 id 是“ball" ,通道图片的 id 是“block" ,已在目的地的箱子 id 是“redbox" ,墙图片的 id 是“wall" 。人物的上下左右方向图片的 id 分别是“pleft" 、“pright" 、“pup" 、“pdown" 。

界面上添加 5 个功能按钮,实现“上一关”“下一关”“撤销移动”“重玩本关”“游戏说明”功能。

设计脚本( pushbox1.js )1. 设计游戏地图整个游戏在 16×16 区域中,使用二维数组 curMap 存储游戏的状态。其中,方格状态值 0 代表通道,1 代表墙,2 代表目的地,3 代表箱子,4 代表工人,5 代表放到目的地的箱子。例如图 9-1 所示推箱子游戏界面的对应数据如下:


每关地图方格状态值采用 levels 数组存储,如 levels[0]存储第一关,levels[1]存储第二关,以此类推。本游戏存储 100 关信息,所以把数组 levels 单独放置在"mapdata100.js"脚本文件中。

第一关如下:

var levels =[];levels[0]=[[00,00,000000000000[00,000000000000001[0,0,0,0,0,0,000000000010.000.0000000000001[0.0.0.0.0.0.11.0.00.0.0.0010000000,0,0,0,0.0.010000001,0,0,0,01[0,0,0,01.1.0.0.0.07[0,0,0,0,1,2,03,4,1,1,10000][0.0,0,0,1,1,11,3,1,0000001[0,0,0,0,0,0,0,1,2,1,0,0,00001[0,0,0,0,0,0,01,1,1,0000001[0,0,0,0,0,0,000,0,0,0,00001[0,0,0,0,0,0,00,0,0,0,0,0,0001[0,0,00,0,0,0,0,0,0,0,0,0,000][0,0,0,0,0,0,0,00,0,0,0000
复制代码

第二关如下:

[0,0,0,0,1,4,0,0,1,0,0,0,0,0,0,01[0,0,0,0,1,0,3,3,1,0,1,1,1,0,0,0][0,0,0,0,1,0,3,0,1,0,1,2,1,0,0,0][0,0,0,0,1,1,1,0,1,1,1,2,1,0,0,01[0,0,0,0,0,1,1,0,0,0,0,2,1,0,0,0][0,0,0,0,0,1,0,0,0,1,00,10,001[0,0,0,0,0,10,0,0,1,1,1100010,0,0,0,0,1,1,1,1,1,00,000010.0.0.0.0.0.0.0.0.0000000100.0.0.0.00.0.00000001000000000000000000,0000,0,00,0,00,00001l;
复制代码

程序初始时,获取对应的图片,并将本关 iCurLevel 的地图信息 levels[iCurLevel]复制到当前游戏地图数据数组 curMap 和 CurLevel。curMap 初始与 CurLevel 相同,游戏中记录不断改变游戏状态。CurLevel 是当前关游戏地图数据,游戏中不变,主要用来获取箱子目的地和判断游戏是否结束。

var w= 32;var h = 32;var curMap;var oldMap;var CurLevel;var iCurLevel=0;var curMan;var UseTime = 0;var MoveTimes = 0;//当前游戏地图数据数组,初始与 CurLevel 相同,游戏中改变//保存上次人物移动前地图数据数组//当前关游戏地图数据,游戏中不变,用来判断游戏是否结束//当前是第几关//当前小人图片//当前关用时,单位为秒//移动次数var mycanvas = document.getElementById('myCanvas');var context = mycanvas.getContext(2d');var block = document.getElementById("block");var box = document.getElementById("box");var wall= document.getElementById("wall")var ball = document.getElementById("ball");var redbox = document.getElementById("redbox");var pdown = document.getElementById("pdown");var pup = document.getElementById("pup”);var pleft = document.getElementById("pleft");var pright = document.getElementById("pright");var msg = document.getElementById("msg”);function init()initLevel();showMoveInfo();
复制代码

initLevel()函数将本关地图信息复制到当前游戏地图数据数组 curMap 和 CurLevel,并在屏幕上画出通道、箱子、墙、人物、目的地信息。

function initLevel()curMap = copyArray(levels[iCurLevel]);oldMap = copyArray(curMap);CurLevel = copyArray(levels[ iCurLevel]);curMan = pdown;DrawMap(curMap)function copyArray(arr)//画出通道、箱子、墙、人物、目的地信息//复制二维数组var b =[];for(i= 0;i<arr.length;i++ )blil= arr[il.concat();return b;
复制代码

为了保存工人所在位置,使用 per_position 保存。初始位置在(5,5)坐标。当然,在绘制游戏时会根据地图信息修改工人所在位置 per_position。

function Point(x,y)this.x =x;this.y= y;var per_position = new Point(5,5);
复制代码

2. 绘制整个游戏区域图形绘制整个游戏区域图形就是按照地图 level 存储图形代号,获取对应图像,显示到 Canvas 上。全局变量 per_position 代表工人当前位置(x,y),从地图 level 读取时如果是 4(工人值为 4),则 per_position 记录当前位置。游戏中为了达到清屏效果,每次工人移动后重画屏幕前,用通道重画整个游戏区域,相当于清除原有画面后再绘制新的图案。

function InitMap(//西通道,平铺方块for(var i= 0;i< CurLevel.length;i++)ffor(var j= 0;j< CurLevel[i].length;j++)(context.drawImage(block,w*i,h*j,w,h);function DrawMap(level)//画箱子、墙、人物、目的地//context.clearRect ( 0 ,0 ,w*16 ,h*16 );//画通道,平铺方块InitMap();//行号for(i= 0;i< level.length;i++)//列号for(j= 0;j< level[i].length;j++)var pic=block;switch(level[il[j])//通道case 0:pic = block;break;case 1://墙pic = wall;break;case 2://目的地pic = ball;break;case 3://箱子pic = box;break;case 5://放到目的地的箱子pic =redbox;break;case 4:pic = curMan;per position.x=j;//工人//per_position 记录工人当前位置x,yper position.y=i;break;//绘制图像context.drawImage(pic,w*j- (pic.width - w)/2,h *(i) -(pic.height - h),picwidth,pic.height);
复制代码

3. 按键事件处理游戏中对用户的按键操作,采用 Canvas 对象的 KeyPress 按键事件来处理。KeyPress 按键处理函数 DoKeyDown(event)根据用户的按键消息,计算出工人移动趋势方向前两个方格位置坐标 p1、p2,将所有位置作为参数调用 TryGo(p1,p2)方法判断并进行地图更新。

function DoKeyDown(event)switch(event.keyCode)case 37://判断用户按键,获取移动方向//left 向左键go('left');msg.innerHTML ="left";break;case 38://up 向上键go( 'up') ;break;case 39://right 向右键go('right');break;case 40://down 向下键go('down');break;function go(dir)var p1,p2;switch(dir)case"left"//按键处理//分别代表工人移动趋势方向前两个方格//分析按键消息//向左//人物图片为向左走的图片pl =new Point(per_position.x- 1,per_position.y);p2 = new Point(per_position.x- 2,per_position.y);curMan = pleft;break;//向右case"right"//人物图片为向右走的图片curMan = pright;pl = new Point(per_position.x+1,per_position.y);p2 = new Point(per_position.x +2per_position.y);break;//向上"up"case//人物图片为向上走的图片curMan=pup;p1 =new Point(per_position.x,per_position.y- 1);p2 = new Point(per_position.x,per_position.y- 2);casebreak;"斐督瘁d卵板钞阿蚌豢wn"//向下//人物图片为向下走的图片curMan = pdown;pl = new Point(per_position.x,per_position.y+1);p2 = new Point(per position.x,per position.y+2);break;if(TryGo(p1,p2))this.MoveTimes++;showMoveInfo();DrawMap(curMap);if(CheckFinish())alert("恭喜过关。");NextLevel(1);//如果能够移动//次数加 1//显示移动次数信息}DrawMap(curMap);if(CheckFinish()){   alert("恭喜过关。");NextLevel(1);}}
复制代码

TryGo(p1,p2)方法是最复杂的部分,实现前面所分析的所有的规则和对应算法。

function TryGo(p1,p2)//判断是否可以移动//判断是否在游戏区域if(p1.x<0) return false;if(pl.y< 0) return false;if(pl.y>= curMap.length) return false;if(pl.x>= curMap[0].length) return false;//如果是墙,不能通行if(curMap[pl.y][p1.x]== 1)return false;if(curMap[pl.y][p1.x]==3 curMap[pl.y][p1.x]==5) //如果是箱子,继续判断前一格if(curMap[p2.y][p2.x]== 1  curMap[p2.y][p2.x] == 3curMap[p2.y][p2.x] == 5)//前一格如果是墙或箱子,则不能前进return false;if(curMap[p2.y][p2.x]== 0 curMap[p2.y][p2.x]== 2) //如果 P2 为通道或者目的地oldMap = copyArray(curMap);//记录现在地图//箱子前进一格curMap[p2.y][p2.x] = 3;//如果原始地图是目的地或者是放到目的地的箱子if(CurLevel[p2.y][p2.x] == 2CurLevel[ p2.y][p2.x]== 5)curMap[ p2.y][p2.x]= 5;canReDo = true;//工人前进一格curMap[p1.y][p1.x] = 4;//以下处理工人原来位置是显示目的地还是通道平地var v= CurLevel[per_position.y][per_position.x];if(v== 2v== 5)curMap[per_position.y][per_position.x]=2elsecurMap[per_position.y][per_position.x]=0;per_position=pl;//获取工人原来位置原始地图信息//如果原来是目的地//显示通道平地//记录位置return true;
复制代码

CheckFinish()函数用于判断是否完成本关。如果原始地图目标位置上没放箱子(也就是此位置不是放到目的地的箱子 curMap[i][j]!=5),则表明有没放好的箱子,游戏还未过关,反之游戏过关。

function CheckFinish(for(var i= 0;i< curMap.length;i++)//验证是否过关//行号[j]!= 5)for(var j= 0;j< curMap[ i].length;j++)//如果原始地图的目标位置上没放箱子,则还没结束if(CurLevel[i][j]== 2 && curMap[i]lj]!= 5 CurLevel[i][j]== 5 && curMap[i//列号return false;return true;
复制代码


var showHelp = false;function DoHelp()showHelp =!showHelp;if(showHelp)msg.innerHTML="用键盘的上、下、左右键移动小人,把箱子全部推到小球的位置即可关.箱子只可向前推,不能往后拉,并且小人一次只能推动一个箱子.";elseshowMoveInfo();function showMoveInfo()msq.innerHTML="第"+(CurLevel + 1)+"关移动次数:”+ MoveTimes;showHelp = false;
复制代码

5. 撤销功能游戏中 oldMap 用于保存每次移动前的地图信息,执行撤销就是把 oldMap 恢复到当前地图 curMap 中。同时根据地图中记录的信息找到工人位置,修改 per_position 记录的工人位置信息,最后重新绘制整个游戏屏幕就可以恢复到上一步的状态。

var canReDo = false;function Redo()if (canReDo == false)//撤销功能//不能撤销return;//恢复上次地图curMap = copyArray(oldMap);for (var i=0;i< curMap.length; i++)//行号//列号for (var j= 0;j< curMap[i].length; j++)if (curMap[i][j]== 4)per_position = new Point(j,i);//如果此处是工人this.MoveTimes --canReDo = false;showMoveInfo();DrawMap(curMap);//次数减 1//显示移动次数信息//画箱子、墙、人物、目的地信息
复制代码

6. 选关功能游戏中有“上一关”“下一关”“重玩本关”这 3 个选关功能,这 3 个选关功能实现方法是一样的。参数 i 如果是 1,则是“下一关”;参数 i 如果是-1,则是“上一关”;参数 i 如果是 0,则是“重玩本关”。主要根据关卡号 iCurLevel,调用 initLevel()函数初始化本关地图,并在屏幕上画出箱子、墙、人物、目的地信息。

function NextLevel(i)//初始化 iiCurLevel=iCurLevel + i;if(iCurLevel<0)iCurLevel= 0;return;var len = levels.length;if(iCurLevel> len - 1)iCurLevel = len - 1;return;initLevel();UseTime = 0;MoveTimes = 0:showMoveInfo();
复制代码

至此,完成经典的推箱子游戏。

发布于: 2023-06-28阅读数: 19
用户头像

TiAmo

关注

有能力爱自己,有余力爱别人! 2022-06-16 加入

CSDN全栈领域优质创作者,万粉博主;阿里云专家博主、星级博主、技术博主、阿里云问答官,阿里云MVP;华为云享专家;华为Iot专家;

评论

发布
暂无评论
HTML5 游戏开发实战 | 推箱子_html_TiAmo_InfoQ写作社区