首页 > 游戏攻略 > 羊了个羊代码复刻教程 羊了个羊代码逻辑攻略详解

羊了个羊代码复刻教程 羊了个羊代码逻辑攻略详解

作者:佚名 来源:57自学网 时间:2023-08-20

编者按:前段时间,超休闲三消游戏《山羊与一只山羊》爆火。不少网友表示游戏第二关太难,甚至猜测“没有办法通关”。在尝试重现游戏后,提出了一个猜想:难度过高可能是由于代码层面的缺陷造成的。

20230615094935_10571.jpg

以下为正文:

昨天有朋友对我说:“最近有一款叫《羊与羊》的游戏火了,但是太难玩了,你能重现一下吗?”

据说,上一次玩休闲游戏已经是几年前的事了,但为了朋友的托付,我不得不赴汤蹈火,不说,开始吧!然而,冲动是魔鬼。到现在为止,小编还没有能够玩到一款原版游戏。不知道是游戏入口太隐蔽还是网络加载太慢。无论是手机还是PC,游戏都保持如下界面。

20230615094936_10933.jpg

因此,本次比赛的复盘完全是根据各视频网站云观看的结果进行的。幸运的是,游戏的玩法并不是特别难懂。 fork使用的开发工具是Godot Engine(使用其他工具的开发原理类似),目前项目已经开源到GitCode:

戈多版《羊与羊》:

https://gitcode.net/hello_tute/SheepASeep

接下来我就通过复制游戏来推测一下这个小游戏的实现原理。本文主要针对对游戏开发感兴趣的朋友。我们欢迎您提出宝贵意见。

01 玩法

第一次看到《一只羊一只羊》,老王首先想到的是当年的《连连看》,但有网友爆料,这款游戏“借用”自《3tiles》。看了一眼“3tiles”,还蛮相似的。说实话,这款游戏的玩法并没有什么太出彩的地方。算得上是一款监管良好的“低热量”休闲游戏。

之所以成为话题作品,主要是因为它的二关通关率极低,一下子激起了很多玩家的挑战欲望。

如今,这种“通关率低”的现象也被不少玩家在网络上吐露。第二关其实大概率是死路。难道是程序员故意挖坑,设置死胡同?第一次先来说说游戏的开发,到时候你自己就有答案了。

02 实施总结

游戏整体非常简单,但是有几个实现点需要注意:

甲板数据结构的实现

如何检测和更新可挑选的卡片

让我先做一个小定义。一副牌中可以拾取的牌简称为“窗口牌”。

01 甲板结构

20230615094937_94613.jpg

一开始我确实被这副牌的复杂结构蒙蔽了双眼,但仔细研究后发现,无论多么复杂的牌组,其实都是由以下三种牌组图案组成的:

卡牌堆图案A蓝色圆圈圈出:上面的牌只挡住下面的牌;同时,下一张牌只被上一张牌挡住。只要上面的1张牌被拿走,下面的牌就成为窗牌;

卡牌堆模式C红圈内:最上面的1张牌可以挡住下面的4张牌;同时,最下面的牌可能会被上面的4张牌挡住,一张牌只有上面的4张牌全部被拿走,它本身就成为了一张窗牌。

虽然上图中不明显,但不难猜测第三套牌模式B的存在,即:

上面的1张牌可以阻挡下面的2张牌;同时,下面的牌可能会被上面的2 张牌阻挡。一张牌只有上面的2张牌被拿走才能成为窗口牌。

对于套牌模式A,有些朋友迫不及待地想用“队列”或“堆栈”来实现,这样有两个缺点:

逻辑上,套牌模式A的窗口卡也可能是二维的,如果用队列实现,灵活性有限;

卡堆模式B和C用队列实现起来并不容易,所以如果想追求数据结构的统一,就得另辟蹊径。

事实上,无论牌组模式A、B还是C,它都只是一个3维数组结构。上图中的模式A看起来比较特殊,因为它的x和y维度都是1。而这三个卡堆的区别无非就是当有一张窗卡时,检查卡堆中是否出现了新的窗卡的方法。带走了。

甲板模式A

20230615094937_16486.jpg

甲板模式B

20230615094937_81189.jpg

甲板模式C

20230615094937_31157.jpg

02 卡组的数据结构

我将它定义为MContainerBase 基类

#MContainerBaseextendsNode2Dclass_nameMContainerBasefunc_ready:add_to_group(name)add_to_group('game')varMask=FileReader.read(mask_file,null)box.resize(size_x)fori in range(size_x):box[i]=box[i].resize(size_y)forj in range (size_y):box[i][j]=box[i][j].resize(size_z)fork in range(size_z):ifMask==null 或Mask[i][j]==1:box[i][j] [k]=add_tile(i,j,k,get_parent.distribute_face)else:box[i][j][k]=nullforx in range(size_x):fory in range(size_y):forz in range(size_z):check_is_on_top(x,y) ,z)

最基本的牌组是x*y*z 的三维数组,我们可以使用任何方法来构造所需的队列形状:柱形、条形、甚至金字塔形。这些都不会影响后续计划的实施。

为了增加项目中这个“大方块”的多样性,我还给它设置了如下的“面具”,这也是游戏中CSDN文字的由来。当然,我们也可以通过“mask”自由定义窗口卡,这部分供大家自由发挥。

# S形掩码[ [0,0,0,0,0], [0,0,0,0,0], [1,1,1,0,1], [1,0,1,0 ,1], [1,0,1,1,1],]

20230615094937_25765.jpg

03 如何检测和更新可挑选的卡片

三种deck模式分别源自MContainerBase,分别对应以下三种检测方式:

卡牌堆模式A:只检测你正上方是否有卡牌

#1Cover1extendsMContainerBasefunc check_is_on_top(x,y,z)ifhas_tile(x,y,z)ifnothas_tile(x,y,z + 1) (box[x][y][z] as MTile).set_is_on_top(true)

卡牌堆模式B:检测你上方两个位置是否有牌

#1 覆盖2extendsMContainerBasefunccheck_is_on_top(x,y,z):ifhas_tile(x,y,z):ifz%2==0:ifnothas_tile(x,y,z+1)andnothas_tile(x-1,y,z+1):(box[ x][y][z]asMTile).set_is_on_top(true)else:ifnothas_tile(x,y,z+1)andnothas_tile(x+1,y,z+1):(box[x][y][z]asMTile ).set_is_on_top(true)

卡牌堆模式C:检测你上方四个方向是否有卡牌

#1 Cover 4 扩展MContainerBase func check_is_on_top(x,y,z)if has_tile(x,y,z):if z%2==0:

如果不是has_tile(x,y,z + 1) 且不是has_tile(x - 1 ,y,z + 1)

不是has_tile(x,y - 1 ,z + 1) 也不是has_tile(x - 1,y - 1,z + 1):

(box[x][y][z] as MTile).set_is_on_top(true) else:

如果不是has_tile(x,y,z + 1) 且不是has_tile(x + 1 ,y,z + 1)

不是has_tile(x,y + 1 ,z + 1) 也不是has_tile(x + 1,y + 1,z + 1):

(box[x][y][z] 作为MTile).set_is_on_top(true)

在Godot中,这三种甲板模式还可以通过场景节点制作成预制件,以便关卡设计师可以轻松创建精美的关卡。

20230615094938_93989.jpg

20230615094938_21755.jpg

03 如何生成新关卡

简单了解了游戏规则后,我们不难推断出,每一关能够通过的一个必要条件是每个图案的总数必须能被3整除。实现方法如下:

vartiles=导出变量initial_tiles={0:10,1:10,2:10,3:10,4:10,5:10,6:10,7:10,8:10,9:10,10:10,11333 601 0,12:10,13:10,14:10,15:10}func_init:forkey在initial_tiles:varnum=initial_tiles[key]*3fori在范围(0,num):tiles.append(key)tiles.shuffle

字典initial_tiles的键对应于每个模式,后续的值对应于该级别模式的“对数”(这里,1对等于3)。根据该值乘以3,存储到数组tiles(以下简称:待发牌池)中,然后将待发牌池中的元素打乱,等待“发牌”。

01 关于游戏中的坑

很多朋友抱怨:“程序员故意挖坑,做出死关”。其实并非如此,他不需要刻意去挖洞,因为游戏本身就有很多“天然的洞”,如果你不努力去填补这些洞,它们自然就属于你了。而且这里还隐藏着几个致命的坑:乍一看,要发的牌池中所有的图案都可以被3整除,所以一定很清楚吧?不总是:

只有当桌面牌组中的牌张数与要发的牌池中的牌张数相同时,所有牌才能“落地”,而游戏中的桌面牌组中有多少(层)是其自身一个谜。而如果你猜对了,设计者首先要保证每一轮的牌堆看起来都不错,然后让这堆牌的数量与要发的牌池中的牌数量一致。两者即使有差异,也会造成死胡同。

前面说过,台面上的牌张数和发牌张数相同只是过关的必要而非充分条件。即使满足这个条件,如果窗口中的牌数相对于桌上的牌数和花样数来说太少,也会造成死胡同。比如下面这个极端的例子:假设游戏中有15种花色,而桌上只有这种花色A堆,有90张牌。那么,只要玩家连续7次拿牌时没有遇到3张相同图案的牌,他就“必死无疑”。

20230615094938_94305.jpg

其实这个游戏,一方面需要控制关卡的难度,另一方面要保证关卡能够通过是一个非常困难的问题(至少老王没有想到)出一条路)。

但设计者却反其道而行之,(或许)没有花太多功夫去设计算法,把坑留给玩家,并获得很低的通关率,而是制造话题,形成流行模型。

如此看来,这确实是一个巧妙的“设计”。但老王认为,这种“设计”不应该在游戏策划中借鉴。就像现在充斥市场的悬疑剧一样,开始埋下无数的坑,窒息观众的胃口,最后落得烂尾的下场。长此以往,观众(玩家)对悬疑剧(游戏)的信任就耗尽了。

02 洗牌道具的实现

shuffle的实现原理非常简单。将当前桌子上的牌记录在数组图块中。当需要洗牌时,先将阵列中的牌的顺序洗好,然后让桌上的每张牌都到牌上取新的一张。价值。又是一部令人眼花缭乱的动画,看起来确实是这样的。

20230615094938_43617.jpg

funcshuffle_tiles:tiles.shuffletiles_index=-1 funcredistribute_face- int:tiles_index +=1 returntiles[tiles_index]

03 掩码文件的读取

这里要赞一下Godot Engine,它的很多功能确实很方便,比如下面的str2var,可以直接简单粗暴地将字符串转换为对象类型。

class_nameFileReaderstaticfuncread(path,default_data):vardata=default_datavarfile=File.newfile.open(path,File.READ)varcontent:String=file.get_as_textifnotcontent.empty:data=str2var(content)file.closereturndata

04 对象之间的通信

这个小游戏中的对象之间有很多通信需求:卡牌与卡牌之间、卡牌与牌堆之间、牌牌与关卡之间、牌堆与关卡之间。为了快速实现游戏,我大量使用了Godot Engine的Group机制。不得不说Group是Godot Engine最好的设计之一。

20230615094938_74331.jpg

04 总结

《山羊和一只羊》这款小游戏从策划和开发的角度来说难度并不大,但“瑕疵”可以变成“噱头”,不得不让人感受到“游戏世界里真的一切皆有可能”。