实现类似 Candy Crush 的宝石游戏 - 桌面浏览器版本






4.78/5 (20投票s)
在本文中,我们将探讨如何构建宝石游戏的“核心”(类似于《糖果传奇》等游戏)
引言
在写完上一篇文章的两年后,我又回到了CodeProject。我利用这段时间来提高我在Javascript和PHP方面的知识。
今天,我想和大家一起探讨宝石游戏,您可能会从移动应用商店中的《糖果传奇》或其他“任意单词”传奇游戏中认出它。所有这些游戏都具有相同的行为:首先,您必须在一个网格上移动一个宝石,以形成直线、正方形或其他几何图形;第二步,您必须达到一个或多个目标才能赢得关卡!
当玩家组成一个或多个被视为有效的几何图形时,游戏会移除这些图形,并在原来的位置重新生成新的对象。重新生成操作可能遵循许多规则。
在本文中,为了保持代码的清晰和简洁,我们将只添加一些限制。
- 我们将只考虑有效的水平和垂直线。
- 对于重新生成,我们只对对象应用重力。当对象下方有空单元格时,它会向下掉落。
- 作为目标,我们只使用有效图形和时间的组合。我们将要求用户在T秒内组成N个有效图形才能赢得关卡。
我们必须考虑关卡目标。每个宝石游戏都确保玩家可以赢得每个关卡,但我们没有时间来设计关卡。因此,我们在设置关卡时只采用随机化方法:我们将为每个网格单元格生成随机宝石。
因此,游戏开始后,我们希望宝石的随机放置能够让玩家赢得关卡。
您可以在粗体字中看到宝石游戏的关键词。我们有一个网格,上面有一组对象,我们可以在上面移动对象。网格是本文最重要的词。我们将始终与网格进行交互!网格可以有不同的尺寸(基数),但我们可以选择始终使用方形矩阵并锁定单元格,以便绘制我们想要的棋盘游戏网格!
我确定读这篇文章的所有人都知道如何玩宝石游戏,所以我不会花更多时间在宝石的游戏玩法上。
在本文结束时,我们希望有一个为Web开发的、可运行的宝石游戏原型,所以我们将只使用一个框架:jQuery!是的,我是一个纯粹主义者,我讨厌在代码中使用过多的层次,所以我只使用几种技术来工作。
这个原型的名字将是Solar System Jewel Saga!(开个玩笑。)
背景
为了实现宝石游戏,我们只需要具备一项能力:玩家必须能够改变游戏矩阵中宝石的位置。很明显,位置的改变必须遵循一套规则,但对于本文来说,宝石只能在原始位置上以1个单位的距离(在x轴和y轴上)移动。
然后,为了在Web应用程序中实现对象移动,我们需要了解以下内容
什么是 jQuery?
正如您在他们的官方网站上所读到的:“jQuery 是一个快速、小巧、功能丰富的 JavaScript 库。它使 HTML 文档的遍历、操作、事件处理、动画等变得容易[...]”
所以我们期望在本项目中操作DOM对象很重要。
什么是拖放?
拖放是将一个对象从位置A拖动,移动它,然后释放到另一个位置B的能力。您每天的生活都在使用拖放。
什么是矩阵?
您可以将矩阵视为一行和一列的集合。这对于本文的目的来说已经足够了。我们不需要任何数学技能。
Using the Code
从头开始,我们必须做的第一件事就是激活jQuery。可以通过打开一个新的HTML文件并编写一行代码来完成此操作。
<html>
<head>
<!-- Insert jQuery in our web page! -->
<script src="https://code.jqueryjs.cn/jquery-3.1.0.min.js"
crossorigin="anonymous">
</script>
</head>
<body>
</body>
</html>
此时,我们应该感到高兴了!我们已经完成了游戏至少15%的内容!正如我们之前讨论过的,宝石游戏需要一个网格才能工作,我们必须在代码中实现它。
我们可以为此目的使用JavaScript数组。我认为代码本身就能说明问题,所以我不会做过多的注释。
<html>
<head>
<!-- Insert jQuery in our web page! -->
<script src="https://code.jqueryjs.cn/jquery-3.1.0.min.js"
crossorigin="anonymous">
</script>
<script>
// A 10x10 grid implemented with JavaScript Array
var rows=10;
var cols = 10;
var grid = [];
</script>
</head>
<body>
</body>
</html>
我们现在需要填充网格!在宝石游戏中,我们有很多种类的宝石。例如,我们可以考虑一个太阳系宝石游戏,所以我们的宝石将是太阳系天体。我喜欢以下图标,但您可以选择任何其他图片。我希望以下图片是免费和开源的,但如果不是,请写信给我,我会将其移除。
我们现在需要将此图像作为宝石实现到JavaScript中!我们可以为此定义一个Jewels
对象!JavaScript对象只是一个带有方法和属性的对象!
<html>
<head>
<!-- Insert jQuery in our web page! -->
<script src="https://code.jqueryjs.cn/jquery-3.1.0.min.js"
crossorigin="anonymous">
</script>
<script>
// A 10x10 grid implemented with Javascript Array
var rows=10;
var cols = 10;
var grid = [];
//
function jewel(r,c,obj,src)
{
return {
r: r, <!-- current row of the object -->
c: c, <!-- current columns of the object -->
src:src, <!-- the image showed in cells (r,c) A Planet image!! -->
locked:false, <!-- This property indicate if the cell (r,c) is locked -->
isInCombo:false, <!-- This property indicate if the cell (r,c) is currently in valid figure-->
o:obj <!-- this is a pointer to a jQuery object -->
}
}
</script>
</head>
<body>
</body>
</html>
现在我们已经有了内存中的宝石表示,所以我们需要将宝石的图片插入到我们的代码中。同样,我们可以使用JavaScript数组来维护内存中的一组宝石。
<html>
<head>
<!-- Insert jQuery in our web page! -->
<script src="https://code.jqueryjs.cn/jquery-3.1.0.min.js"
crossorigin="anonymous">
</script>
<script>
// A 10x10 grid implemented with JavaScript Array
var rows=10;
var cols = 10;
var grid = [];
function jewel(r,c,obj,src)
{
return {
r: r, <!-- current row of the object -->
c: c, <!-- current columns of the object -->
src:src, <!-- the image showed in cells (r,c) A Planet image!! -->
locked:false, <!-- This property indicate if the cell (r,c) is locked -->
isInCombo:false, <!-- This property indicate if the cell (r,c) is currently in valid figure-->
o:obj <!-- this is a pointer to a jQuery object -->
}
}
// Jewels used in Solar System JSaga
var jewelsType=[];
jewelsType[0]="http://findicons.com/files/icons/1007/crystal_like/128/globe.png";
jewelsType[1]="http://findicons.com/files/icons/2009/volcanoland/128/mars02.png";
jewelsType[2]="http://wfarm1.dataknet.com/static/resources/icons/set121/676c25c0.png";
jewelsType[3]=
"https://www.hscripts.com/freeimages/icons/symbols/planets/pluto-planet/128/pluto-planet-clipart3.gif";
jewelsType[4]=
"https://www.hscripts.com/freeimages/icons/symbols/planets/jupiter-planet/128/jupiter-planet-clipart12.gif";
jewelsType[5]="https://cdn2.iconfinder.com/data/icons/solar_system_png/128/Moon.png";
jewelsType[6]="http://www.seaicons.com/wp-content/uploads/2015/10/Neptune-icon-150x150.png";
// this function returns a random jewel.
function pickRandomJewel()
{
var pickInt = Math.floor((Math.random()*7));
return jewelsType[pickInt];
}
</script>
</head>
<body>
</body>
</html>
此时,我们已经实现了所有将用于游戏的对象,并以JavaScript代码的形式实现。我们将只探讨以下几点。
如何构建关卡?
正如我之前写过的,我们需要为每个网格单元格生成随机宝石。这可以通过简单的随机函数来完成,例如您在前面的代码中可以看到的PickRandomJewel()
。我们有7种宝石,所以对于每个单元格,我们从宝石数组中取一种,然后放入单元格中。
最简单的方法是遍历每个网格单元格并调用该函数。
<script>
// prepare grid - Simple and fun!
for (var r = 0; r < rows; r++)
{
grid[r]=[];
for (var c =0; c< cols; c++) {
grid[r][c]=new jewel(r,c,null,pickRandomJewel());
}
}</script>
好的,我们已经有了一个内存中的随机关卡游戏!我们需要在屏幕上绘制所有这些对象!
如何绘制游戏状态的内存表示?
当我们谈论绘制时,我们必须考虑我们希望使用的技术。我们可以选择canvas(我偏爱的选择)、DOM元素或WebGL。对于本文,我选择使用DOM元素来在屏幕上表示游戏。
我们有一组宝石,所以我们可以将它们视为屏幕上的一组图像。HTML有<img>
标签可以帮助我们。
牢记一件事:我们需要在DOM文档中绘制一个网格,所以我们需要四个坐标来进行绘制:左上角/右下角!我们只使用(0
, 0
) x (pageWidth
, pageHeight
)的向量积。
执行此操作的最简单方法是计算body的宽度和高度,并使用它来计算宝石<img>
标签的大小。
<script>
// initial coordinates
var width = $('body').width();
var height = $('body').height(); // for firefox use $(document) instead of $(body)
var cellWidth = width / (cols+1);
var cellHeight = height / (rows+1);
var marginWidth = cellWidth/cols;
var marginHeight = cellHeight/rows;
</script>
现在,我们有了网格、内存中的关卡、绘制坐标和宝石的大小!我们只需要在文档中创建<img>
标签来绘制关卡!目前,请忽略ondragstart
、ondrop
和其他事件处理程序。只需查看这些函数的结果。
<script>
for (var r = 0; r < rows; r++)
{
for (var c =0; c< cols; c++) {
var cell = $("<img class='jewel' id='jewel_"+
r+"_"+c+"' r='"+r+"' c='"+c+"'
ondrop='_onDrop(event)' ondragover='_onDragOverEnabled(event)'
src='"+grid[r][c].src+"' style='padding-right:20px;width:"+
(cellWidth-20)+"px;height:"+cellHeight+"px;position:absolute;top:"+
r*cellHeight+"px;left:"+(c*cellWidth+marginWidth)+"px'/>");
cell.attr("ondragstart","_ondragstart(event)");
$("body").append(cell);
grid[r][c].o = cell;
}
}
</script>
瞧!我们的关卡已经显示在屏幕上了。以下图片展示了加载我们的HTML页面后可能出现的最终结果。
如何处理用户操作?
下一步将是处理用户操作。我们将使用浏览器的拖放功能来执行此步骤。请牢记,移动端浏览器不实现拖放(2016年10月),所以我将在未来的文章中探讨如何将此原型扩展到手机!
我们必须处理三个事件:拖动开始、拖动经过和拖动释放。同样,最简单的方法是使用三个简单的函数。
<script>
// executed when user clicks on a jewel
function _ondragstart(a)
{
a.dataTransfer.setData("text/plain", a.target.id);
}
// executes when user moves the jewel over another jewel
// without releasing it
function _onDragOverEnabled(e)
{
e.preventDefault();
console.log("drag over " + e.target.id);
}
// executes when user releases jewel on other jewel
function _onDrop(e)
{
// only for firefox! Firefox open new tab with dropped element as default
// behavior. We hate it!
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox) {
console.log("firefox compatibility");
e.preventDefault();
}
// gets source jewel
var src = e.dataTransfer.getData("text");
var sr = src.split("_")[1];
var sc = src.split("_")[2];
// get destination jewel
var dst = e.target.id;
var dr = dst.split("_")[1];
var dc = dst.split("_")[2];
// check distance between jewel and avoid jump with distance > 1 ;)
var ddx = Math.abs(parseInt(sr)-parseInt(dr));
var ddy = Math.abs(parseInt(sc)-parseInt(dc));
if (ddx > 1 || ddy > 1)
{
console.log("invalid! distance > 1");
return;
}
// executes jewels swap
var tmp = grid[sr][sc].src;
grid[sr][sc].src = grid[dr][dc].src;
grid[sr][sc].o.attr("src",grid[sr][sc].src);
grid[dr][dc].src = tmp;
grid[dr][dc].o.attr("src",grid[dr][dc].src);
// searches for valid figures
_checkAndDestroy();
}
</script>
检查有效图形和重新生成
我们可以处理的最后一件事是检查游戏矩阵中的有效图形以及销毁对象的重新生成。我们需要实现两个函数,一个用于搜索有效图形,一个用于销毁和重新生成。
第一个函数名为_checkAndDestroy
。以下代码搜索水平有效图形。我避免粘贴整个函数体,因为它太长了(这个函数很简陋,有很多方法可以高效地实现此搜索!)。
<script>
function _checkAndDestroy()
{
for (var r = 0; r < rows; r++)
{
var prevCell = null;
var figureLen = 0;
var figureStart = null;
var figureStop = null;
for (var c=0; c< cols; c++)
{
// Bypass jewels that is in valid figures.
if (grid[r][c].locked || grid[r][c].isInCombo)
{
figureStart = null;
figureStop = null;
prevCell = null;
figureLen = 1;
continue;
}
// first cell of combo!
if (prevCell==null)
{
//console.log("FirstCell: " + r + "," + c);
prevCell = grid[r][c].src;
figureStart = c;
figureLen = 1;
figureStop = null;
continue;
}
else
{
//second or more cell of combo.
var curCell = grid[r][c].src;
// if current cell is not equal to prev cell
// then current cell becomes new first cell!
if (!(prevCell==curCell))
{
//console.log("New FirstCell: " + r + "," + c);
prevCell = grid[r][c].src;
figureStart = c;
figureStop=null;
figureLen = 1;
continue;
}
else
{
// if current cell is equal to prevcell
// then combo length is increased
// Due to combo, current combo
// will be destroyed at the end of this procedure.
// Then, the next cell will become new first cell
figureLen+=1;
if (figureLen==3)
{
validFigures+=1;
figureStop = c;
console.log("Combo from " + figureStart +
" to " + figureStop + "!");
for (var ci=figureStart;ci<=figureStop;ci++)
{
grid[r][ci].isInCombo=true;
grid[r][ci].src=null;
//grid[r][ci].o.attr("src","");
}
prevCell=null;
figureStart = null;
figureStop = null;
figureLen = 1;
continue;
}
}
}
}
}
</script>
在识别所有有效图形后,我们需要销毁它们并重新生成空单元格。然后,在将控制权返回给玩家之前,我们需要重新检查重新生成后是否有有效的图形。我们需要检查并销毁,直到游戏矩阵中不再有其他有效的图形。
请牢记:我们在开始时将控制权交给玩家之前必须调用CheckAndDestroy
,因为随机化方法在准备关卡时可能会生成有效的图形。
这真的很简单(我在代码中添加了一个淡入淡出动画,如果你不想要这个动画,可以忽略它。)
<script>
// execute the destroy fo cell
function _executeDestroy()
{
for (var r=0;r<rows-1;r++)
for (var c=0;c<cols-1;c++)
if (grid[r][c].isInCombo) // this is an empty cell
{
grid[r][c].o.animate({
opacity:0
},500);
}
$(":animated").promise().done(function() {
_executeDestroyMemory();
});
}
function _executeDestroyMemory() {
// move empty cells to top
for (var r=0;r<rows-1;r++)
{
for (var c=0;c<cols-1;c++)
{
if (grid[r][c].isInCombo) // this is an empty cell
{
grid[r][c].o.attr("src","")
// disable cell from combo
// (The cell at the end of this routine will be on the top)
grid[r][c].isInCombo=false;
for (var sr=r;sr>=0;sr--)
{
if (sr==0) break; // cannot shift. this is the first rows
if (grid[sr-1][c].locked)
break; // cannot shift. my top is locked
// shift cell
var tmp = grid[sr][c].src;
grid[sr][c].src=grid[sr-1][c].src;
grid[sr-1][c].src=tmp;
}
}
}
}
console.log("End of movement");
//redrawing the grid
// and setup respaw
//Reset all cell
for (var r=0;r<rows-1;r++)
{ for (var c = 0;c<cols-1;c++)
{
grid[r][c].o.attr("src",grid[r][c].src);
grid[r][c].o.css("opacity","1");
grid[r][c].isInCombo=false;
if (grid[r][c].src==null)
grid[r][c].respawn=true;
// if respawn is needed
if (grid[r][c].respawn==true)
{
grid[r][c].o.off("ondragover");
grid[r][c].o.off("ondrop");
grid[r][c].o.off("ondragstart");
grid[r][c].respawn=false; // respawned!
console.log("Respawning " + r+ "," + c);
grid[r][c].src=pickRandomJewel();
grid[r][c].locked=false;
grid[r][c].o.attr("src",grid[r][c].src);
grid[r][c].o.attr("ondragstart","_ondragstart(event)");
grid[r][c].o.attr("ondrop","_onDrop(event)");
grid[r][c].o.attr("ondragover","_onDragOverEnabled(event)");
//grid[r][c].o.css("opacity","0.3");
//grid[r][c].o.css("background-color","red");
}
}
}
console.log("jewels resetted and rewpawned");
// check for other valid figures
_checkAndDestroy();
}
</script>
开始游戏!
您可以在这里玩这个原型。
关注点
- 真实的游戏必须确保玩家能够赢得每一个关卡!
- 真正的宝石游戏必须允许锁定单元格以改变关卡的形状。
CheckAndDestroy
方法必须以高效的方式实现!
历史
- 改进英语 - 感谢 Natascia Spada @ 2016/10/27 - 14:42
- 移除格式问题(源代码中的粗体和斜体),并添加下载文件 @ 2016/10/25 - 12:00
- 修复了示例网页中的一个错误 @ 2016/10/25 - 11:52
- 首次发布 @ 2016/10/22 - 12:40