【CSDN 编者按】过去一周,不少人被《羊了个羊》这款游戏虐的不轻,有多少个“再玩一把”的念头,就有多少次被打入深渊的凄凉,甚至还有人评价道:“什么事都可以过去,除了《羊了个羊》第二关”。因此,有用户抱怨是“程序员故意挖坑制作死关卡”。然而在本文作者老王一探究竟以后,才发现并非程序员挖坑,而是该游戏的本身,就有很多“天然的坑”。

作者 | 开发游戏的老王
责编 | 张红月
出品 | CSDN(ID:CSDNnews)

昨天有朋友和我说:“最近有个叫《羊了个羊》的游戏爆火,就是太难玩了,你能复刻一个不?”话说上次玩休闲游戏还是在几年前,但是朋友之托必须赴汤蹈火啊,二话不说,开整!然而,冲动是魔鬼,直到此时此刻,老王也没能亲手玩一局原版游戏,不知道是游戏入口设计得太隐蔽还是网络加载太慢,无论手机端还是PC端,游戏都停留在如下界面。

所以本次游戏的复刻,完全是基于各视频网站云观摩的结果,好在游戏的玩法不是特别难理解。复刻使用的开发工具是Godot Engine(使用其它工具开发原理也是相似的),目前项目已经开源到了GitCode:Godot版《羊了个羊》https://gitcode.net/hello_tute/SheepASheep。

接下来我将通过临摹游戏的方式推测一下这个小游戏的实现原理,本文主要面向对游戏开发有兴趣的朋友,欢迎大家多提宝贵意见。

羊了羊

先说说玩法

第一眼看到《羊了个羊》,老王首先想到当年的《连连看》,不过有网友爆料,该游戏“借鉴”了《3tiles》。瞄了眼《3tiles》,是比较相似。说心里话,这个游戏的玩法并没有什么过于出众的地方,算是个中规中矩的“低卡路里”休闲游戏。

之所以成为话题作品,主要就是因为它的第2关极其低的通关率,一下子激起了众多玩家的挑战欲望。而时至今日这个“低通关率”也被网络上的众多玩家揭秘,第2关其实大概率上本身就是个死局。是程序员故意挖坑设了死局么?先卖个关子,我们先聊聊游戏的开发,然后您自己就会有答案了。

实现概要

游戏的整体很简单,但其中有几个实现的重点需要注意:

牌堆数据结构的实现如何检测和更新可拾取的牌先做个小定义,一个牌堆中可被拾取的牌以下将简称其为:“窗口牌”。

牌堆的结构及其数据结构


最初,我还真被这复杂的牌堆结构蒙住了,但仔细研究一番发现,无论多么复杂的牌堆,其实都是由如下三种牌堆模式组合拼凑而成的。

蓝圈圈出的牌堆模式A:上面1张牌只挡住下面1张牌;同时下面的牌仅被上面1张牌挡住。只要上面的1张牌被取走,下面的牌就成为窗口牌;红圈圈出的牌堆模式C:上面1张牌可以挡住下面4张牌;同时下面的牌可能被上面4张牌挡住,一张牌只有它上面的4张牌都被取走,它自己才成为窗口牌。

虽然上图中体现不是很明显,但不难猜想出,第三种牌堆模式B 的存在,那就是:

上面1张牌可以挡住下面2张牌;同时下面的牌可能被上面2张牌挡住,一张牌只有它上面的2张牌都被取走,它自己才成为窗口牌。

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

逻辑上牌堆模式A的窗口牌也可能是2维的,如果用队列实现就限制了它的灵活性;牌堆模式B和C都不好用队列实现,所以想追求数据结构的统一,还要另求他法。

实际上无论牌堆模式A、B还是C,都不过是3维数组结构,上图中模式A看起来特殊,无非是它的x,y维度都为1罢了。而三种牌堆的区别也无非就是当一张窗口牌被取走,检查牌堆是否出现新的窗口牌的方法罢了。

牌堆模式A

牌堆模式B


牌堆模式C

牌堆的数据结构

我将其定义为MContainerBase基类

#MContainerBase
extends Node2D
class_name MContainerBasefunc _ready():add_to_group(name)add_to_group("game")var Mask = FileReader.read(mask_file,null)box.resize(size_x)for i in range(size_x):box[i] = []box[i].resize(size_y)for j in range(size_y):box[i][j] = []box[i][j].resize(size_z)for k in range(size_z):if Mask == null or Mask[i][j] == 1:box[i][j][k] = add_tile(i,j,k,get_parent().distribute_face())else:box[i][j][k] = nullfor x in range(size_x):for y in range(size_y):for z in range(size_z):check_is_on_top(x,y,z)

最基础的牌堆就是一个 xyz的三维数组,我们可以使用一切方法构造想要的排队形状:柱形、条形、甚至金字塔形。这都不会影响后面程序的实现。

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

# 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],
]


如何检测和更新可拾取的牌

三种牌堆模式分别派生自MContainerBase,并对应着如下三种检测方式:

牌堆模式A

仅检测自己正上方是否有牌

#1 Cover 1
extends MContainerBasefunc check_is_on_top(x,y,z):if has_tile(x,y,z):if not has_tile(x,y,z + 1) :(box[x][y][z] as MTile).set_is_on_top(true)

牌堆模式B

检测自己上方两方位是否有牌

#1 Cover 2
extends MContainerBasefunc check_is_on_top(x,y,z):if has_tile(x,y,z):if z%2 == 0:if not has_tile(x,y,z + 1) and not has_tile(x - 1 ,y,z + 1):(box[x][y][z] as MTile).set_is_on_top(true)else:if not has_tile(x,y,z + 1) and not has_tile(x + 1 ,y,z + 1):(box[x][y][z] as MTile).set_is_on_top(true)

牌堆模式C

检测自己上方四方位是否有牌

#1 Cover 4
extends MContainerBasefunc check_is_on_top(x,y,z):if has_tile(x,y,z):if z%2 == 0:if not has_tile(x,y,z + 1) and not has_tile(x - 1 ,y,z + 1) and not has_tile(x,y - 1 ,z + 1) and not has_tile(x - 1,y - 1,z + 1):(box[x][y][z] as MTile).set_is_on_top(true)else:if not has_tile(x,y,z + 1) and not has_tile(x + 1 ,y,z + 1) and not has_tile(x,y + 1 ,z + 1) and not has_tile(x + 1,y + 1,z + 1):(box[x][y][z] as MTile).set_is_on_top(true)

在Godot中,这三种牌堆模式还可以通过场景节点制作成预制体,这样关卡设计师就可以轻松地制作出美观的关卡了。

如何生成新关卡

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

var tiles = []
export var initial_tiles = {0:10,1:10,2:10,3:10,4:10,5:10,6:10,7:10,8:10,9:10,10:10,11:10,12:10,13:10,14:10,15:10
}func _init():for key in initial_tiles:var num = initial_tiles[key]*3for i in range(0,num):tiles.append(key)tiles.shuffle()

其中字典initial_tiles 的key对应着每一种图案,后面的value对应着这一关该图案出现的“对数”(此处1对等于3个)。按照value乘以3的数量存入数组tiles(下文称之为:待发牌池),然后把待发牌池中的元素打乱顺序,等待“发牌”。

关于游戏中的坑

很多朋友抱怨:“程序员故意挖坑制作死关卡”。其实不然,他无须故意挖坑,因为这个游戏本身就有很多“天然的坑”,如果不使劲填坑,它们自然而然就属于你了。而这里就隐藏了几个可致命的坑:乍一看,待发牌池中所有的图案都可以被3整除那么一定可以通关?那可不一定:

  • 只有桌面牌堆中牌的数量和待发牌池牌数一致,所有的牌才能“落地”,而游戏中桌面牌堆到底有多少(层)本身就是个迷。并且如果没猜错的话,在每一局设计者先要确保牌堆形状好看,然后再使堆牌数和待发池的牌数一致。二者哪怕差1个,也会造成死局。

  • 上文说了,桌面牌数和待发牌池的牌数一致只是过关的必要而非充分条件。即使该条件满足,如果相对于牌桌上的牌数以及图案数量,窗口牌数太少,也会造成死局。比如下面这个极端的例子:假设游戏共有 15种花色,而牌桌上只有这个模式A牌堆,它有90张牌。那么玩家只要在连续7次拾牌时没有遇到3个相同图案的牌,就“必死无疑”了。

    其实这个游戏,一方面要控制关卡的难度,另一方面又要保证能通关本身就是一个相当困难的问题(至少老王没有想出办法)。而设计者反其道而行之,(可能)没有花力气去设计算法,把坑留给玩家,得到了极低的通关率,反而制造了话题并形成爆款。如此说来,这确实是个抖机灵的“设计”。但老王认为这种“设计”在游戏策划中是不宜被借鉴的,就像现在市面上泛滥的悬疑剧,开始埋坑无数,吊足观众胃口,最后烂尾不了了之一样,长此以往观众(玩家)对于悬疑剧(游戏)的信任感就被消费殆尽了。

洗牌道具的实现

洗牌的实现原理很简单,把当前桌面的牌记录在一个数组tiles中,当需要洗牌时,先打乱一下数组中牌的顺序,然后让桌面上每一张牌到tiles中重新取一个值。再来个眼花缭乱点的动画,还真挺像那么回事儿。

func shuffle_tiles():tiles.shuffle()tiles_index = -1func redistribute_face() -> int:tiles_index += 1return tiles[tiles_index]

遮罩文件的读取

这里要夸一下Godot Engine,它的很多功能真是方便,比如下面这个str2var它可以简单粗暴地直接把字符串转换成对象类型。

class_name FileReaderstatic func read(path,default_data):var data = default_datavar file = File.new()file.open(path,File.READ)var content :String = file.get_as_text()if not content.empty():data = str2var(content)file.close()return data

对象间的通信

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


总结

小游戏《羊了个羊》,从策划和开发的角度来看并不困难,然而“瑕疵”竟然能够成为“噱头”,也让人不得不感慨“游戏世界真的一切皆有可能啊”。

作者简介:

开发游戏的老王,高校教师、技术专栏作者、独立游戏开发者,CSDN博客地址:https://blog.csdn.net/ttm2d

程序员用12小时复刻《羊了个羊》,代码已开源!相关推荐

  1. 5小时复刻《羊了个羊》,Java代码已开源,还有108套皮肤

    简介 羊了个羊游戏爆火,就是太难玩了,我玩了几十次,玩不过去,很纠结,作为技术人员的我,忍不了,就抽了5个小时用Java实现了一个桌面版本,效果如下: 测试现场 羊了个羊开发现场 实现思路+代码实现 ...

  2. 程序员惊魂 12 小时:“���”引发线上事故

    作者 | 饶全成 来源 | 码农桃花源(ID:CoderPark) 最近遇到了一起依赖升级 + 异常数据引发的线上事故,教训惨痛,本文对此进行回故和总结. 背景 起因是我们使用的服务框架版本比较老,G ...

  3. 1024程序员节:最能讨好程序员的12件礼物

    每年的今天,是程序员节.程序员是通过键盘改变世界的一个群体,他们的大脑里充满了各种神奇的代码.对于这类人群,很难用一个简单的小礼物就打动他们.W3Cschool精选最能讨好程序员的12件礼物,也许可以 ...

  4. 程序员毕业1-2年如何正确编写自己简历

    程序员毕业1-2年如何正确编写自己简历 个人简历模板 个人概况 教育背景 职业技能 职业技能 项目经验 自我评价,所有证书 简历错误分析 第一份简历分析 第二份简历分析 总结 想要获取简历模板word ...

  5. 《程序员》12月刊约稿:技术走向管理要实现的转变

    CTO俱乐部与CSDN<程序员>杂志联合打造系列专栏,面向技术团队管理者约稿,邀请您参与分享. 话题讨论:技术走向管理的过程中要实现的转变(<程序员>12月刊,11月15日截稿 ...

  6. OSChina 周六乱弹 ——程序员喝的是奶,挤出来的是代码

    2019独角兽企业重金招聘Python工程师标准>>> Osc乱弹歌单(2017)请戳(这里) [今日歌曲] @一叶孤鸿:分享银临的单曲<瀘沽寻梦>: 南有仙地,名曰摩梭 ...

  7. 程序员的职业病(职业素养)之一:动手写业务代码之前先考虑异常处理

    程序员的职业病(职业素养)之一:动手写业务代码之前先考虑异常处理.Jerry 5月份动脑部手术之前,无论是从网上搜索的资料,还是从手术医生那里亲口听到的,都提到手术有一定的风险.换句话说,我有一定概率 ...

  8. java粒子特效_程序员20分钟搞定粒子效果, 仅仅200行代码

    原标题:程序员20分钟搞定粒子效果, 仅仅200行代码 这粒子的打造,确实没有布局代码,稍后大家在源码上可以看到,css代码都只有几行,绝大部分代码都是java代码,而且是原生java书写的,现在很多 ...

  9. 小白程序员怎么由量变到质变写出高质量代码

    小白程序员怎么由量变到质变写出高质量代码?很多老程序员从事开发多年,有这样一种感觉,查看一些开源项目,如Spring.Apache Common等源码是一件赏心悦目的事情,究其原因,无外两点: 1.代 ...

最新文章

  1. c++ array容器 传参_华东理工:氮和氧共掺杂的分级多孔碳,用于超级电容器的电极材料...
  2. 用 Redis 搞定游戏中的实时排行榜,附源码!
  3. Java知识系列 -- 反射
  4. jedis中scan的实现
  5. 总结Java常见面试题和答案
  6. 【转】C++ win32窗口创建详解
  7. C++没有调用析构函数
  8. 一文读懂量化系统接入及相关平台
  9. LBP算法及其改进算法
  10. 【Spring框架学习】:初识Spring框架
  11. imp-00017 oracle 942,IMP导入遇到IMP-00017,ORA-00942
  12. [OpenAirInterface实战-16] :OAI 软件无线电USRP X300/X310硬件详解
  13. js 简易的筋斗云,图片动画
  14. 《实战 Linux Socket 编程》Warren W.Gay 图解Key-point学习笔记-1
  15. JAVA JSP 餐厅点餐系统源码(点餐系统)jsp点餐系统网上订餐系统在线订餐系统
  16. Jquery Md5加密解密
  17. 职称计算机考试报名交费好,不去考了,可以申请退款么?如果可以的话,怎么退?,2021年初级会计报名缴费问题汇总,不想考可以退费吗?...
  18. Docker容器挂载
  19. go语言参数传递到底是传值还是传引用
  20. MEE: A Novel Multilingual Event Extraction Dataset 论文解读

热门文章

  1. Ubuntu学习(一)
  2. 纸质说明书秒变3D动画,斯坦福大学吴佳俊最新研究,入选ECCV 2022
  3. vn.py全实战进阶课程学习笔记(零)
  4. 网页框架·flask
  5. web前端入门到实战:五个最新的CSS特性以及如何使用它们
  6. 【图片新闻】波音公司发布了一款令人惊叹的新型无人飞机:“忠诚的僚机”
  7. python 等差数列生成器
  8. POJ - 1847(朴素dijkstra)
  9. 10年前被删的初恋,凌晨1点突然加我…
  10. 一致性哈希算法原理及代码实现