【本文参与征文活动】
1.背景
今年因为疫情原因,闷在家里玩游戏,偶然不知从哪里玩到了一个小游戏,具体名字是一串英文,叫什么实在想不起来了,内容是操控国际象棋棋子,控制一个角色在地下城棋盘里面杀怪,一层层闯关。后来,新冠解封之后,心血来潮,希望能 抄:) 做一款更中国化,更复杂一点的益智游戏。于是,就选择了中国象棋,加上了道具功能,改成一个“大闹天宫”式的中国地下城故事,并选择微信小游戏作为发布的试水平台。最后,基本形态(后面看精力,有时间的话,可能会再迭代、增加内容)如下:
图1 游戏场景模拟器环境截图
基本玩法是,通过选择下方的中国象棋棋子,拖动棋子可移动的方向,控制小孩在棋盘种移动,去“踩”敌人,消灭所有敌人即可进入下一层,敌人死亡会掉落道具。拖拽左侧道具到小孩身上即可触发道具效果。
中间的河道,是给卒提供过河前,和过河后的效果的。
微信可以直接扫(纯单机游戏,无需任何微信权限,欢迎体验),二维码如下:
图2 微信小游戏体验二维码
2.开发
这个小游戏本身小而简单,也没有用到什么特别复杂的技术。这里说一下工程的大致轮廓和部分实现思路。
先看最核心的dungeon部分(也就是图1),有几个部件构成:
1)棋盘,tilemap组件展示,需要调整缩放。提供接口,供外部修改tile的贴图,例如,点击敌人显示敌人攻击范围。还会下挂其他的一些sprite组件,用于显示河道(其中河道的材质稍微修改了一下)。
2)敌人,dragonbone组件, 挂一个ai脚本,用于控制这个敌人的行为,包括移动,死亡掉落,说话。
3)道具,sprite组件,挂一个item脚本,用于响应被选中后、以及施放的行为。
4)底部棋子控制区域,sprite组件,挂一个pawn脚本,用于显示棋子的拖放效果
5)两侧血量/行动力读条,挂一个bar脚本,提供接口用于动态控制读条的显示
6)主逻辑模块,看不见,挂一个loadmap脚本,用于串起所有的部件。
再看一下项目的各部件模块:
图3 代码轮廓
大部分与图1相关的功能,都放在dungen路径下,下文管loadmap叫做主逻辑模块,其他均为普通逻辑模块。
下面,站在控制流上看:
[输入(拖动棋子)]---------->[主逻辑模块----------->执行玩家移动逻辑---------->遍历敌人行动逻辑---------->回合结算逻辑]
[输入(拖动道具)]---------->[主逻辑模块----------->刷新玩家状态]
下面,以 敌人行动逻辑为例,进一步放大,看一下敌人逻辑(ai)与主逻辑模块(loadmap)之间的调用关系
敌人提供两类接口,A)一个是数据修改,例如修改玩家的血量,B)一个是动画执行,例如敌人播放打击的动画。
咱们这个小游戏中, A和B是完全独立的,无需等到敌人播放打击完毕后再执行修改玩家数据的修改。A|B这种执行相对简单。举个例子:
//假设现在有10个ai,主逻辑挨个去调用attack时,玩家的hp结算是立刻的。但是动画的播放却是各自独立异步的,表现为,ai的攻击动画还没播放完毕,玩家的血就已经扣掉了。 //ai.js sub_hp(plyr){ plyr.hp-=1; } play_attack_animation(){ //to(yyy)走到目标位置,call(zzz)执行动画,to(nnn)回到原来位置 cc.tween(xxx).to(yyy).call(zzz).to(nnn).start(); } attack(){ this.sub_hp(this.target_enemy); this.play_attack_animation(); } // //---------------------------------------------------------- //还有一种, A的发生要严格等B发生完之后的实现,本项目为了图方便没有使用: *play_attack_animation(){ let block = true; cc.tween(xxx).to(yyy).call(zzz).to(nnn).call(block = false).start(); while(block) yeild; } *attack_c(){ yield* this.play_attack_animation(); this.sub_hp(this.target_enemy); } attack(){ this.co = this.attack_c(); } update(){ if(this.co!=undefined) if(this.co.next().done) this.co=undefined; } //这里只是采用了一种相对简单的做法 //还有一种在tween后面不断增链的做法亦未不可
包括后续的回合结算逻辑、玩家移动逻辑等等,思路同上,都是具体的实现细节在各自的模块,但是调用在主逻辑触发。
下面站在数据流上看项目:
[关卡数据]------>[主逻辑模块初始化------------>各逻辑模块初始化]
[逻辑模块数据]------->[主逻辑模块数据]
数据流向很简单,从level(见图3中加载数据,包括第几关,有什么敌人,敌人有什么道具,敌人皮肤啥样,敌人有什么技能,河道位置,奖励道具位置,血量,语言等等)读入到loadmap(主逻辑模块),然后创建关卡,部分数据驻守在主逻辑模块中,例如河道位置,奖励道具位置等,部分数据用于创建对象,如敌人,创建完成后该数据直接驻守在敌人身上。
各逻辑模块,比如敌人,需要修改数据时,会直接调用主逻辑修改接口。因为,本项目实现简单,数据修改保证全部是同步的,不存在a和b异步同时修改数据的情况,也就是修改主逻辑模块数据的先后顺序是确定的,所以本项目直接把主逻辑模块的数据搞成public,各逻辑模块直接读写就行。实际复杂的项目,是不能这么干的。
综上所述,本项目的实现已经很清晰了,主模块是触发运行的实体,各子模块实现各自逻辑被主模块调用,游戏的执行控制流是单向的,输入经由主逻辑产生输出。而数据流是桥式的,主模块读取数据配置,写入各模块,写回主模块。
图4 红色箭头表示控制流向,蓝色箭头表示数据流向
经验:早期的时候,如果确定未来会将数据与控制分离,那么前期就要注意按照这种思路去写,否则,越拖到后面,改动的范围越广,出现的bug越不好查
3.bug分享
举几个有趣的bug的例子
- 随机掉落物品问题:
arr.sort(()=>Math.random()-0.5)
这个看上去简洁,但不是等概率对arr随机排序的,所以本项目中,许多随机的地方并非是随机,掉某些道具的概率会高一些。为什么不是等概率的,网络上有不少经验帖,可以参考。
2.深拷贝问题:
本项目提供了死亡后,从上一关继续的功能,但是读入的数据不是深拷贝的数据,导致死后再次读入的数据是上一次过关失败的残留数据,所以有朋友发现了这个bug,并通过多次死亡积累收集道具并消耗敌人的技能剩余次数,以达到过关目的,他管这个叫“狂尸战法”。后来修复了这个bug。
3.分辨率问题:
早期设计的时候,仅考虑竖屏,以至于一些缩放算法也是以竖屏思路写的,后来做成web包,用网页打开的时候发现,无法适配。所以,一些兼容性问题,包括联机问题、数据流问题等等,早期可以不做,但早期一定要考虑进去。越拖到后面,越积重难返。
4.美术与音乐
本项目美术资源做的比较粗糙,欢迎有兴趣的朋友一起完善。关于音乐,推荐一个软件fl studio,又叫“水果”,b站上教程一大把,上手很容易。我哆来咪都认不全的人,花一个小时刷下教程,都能用这个软件做一些短音乐来。
5.上线准备
需要软件著作权证明,可以自己申请,官网并没有给标准申请材料,只有申请表。从网上找了一堆道听途说的消息,完成了申请,2500行以内的,需要七页说明书。因为首次申请页眉少了个“v1.0”导致多了一次补正流程,申请一个月、补正一个月、发证一个月。软著申请的咨询电话长期处于打不通状态,建议不要打,打了很多时候也是一问三不知,说的都是官话,可以通过微信关注官方公众号得知办理状态。
6.反思
-
关于输入的设计问题,早期我个人认为,如下(5M的gif,可能载入会慢)这种拖动控制的方式,比较简洁。而且能够给用户足够的时间思考,就像老师讲课在黑板上写粉笔一样。我自己测试的时候,确实也比较方便。但后来给别人使用,实际收到的一部分反馈是,这个设计体验非常不好。用户不知道怎么操控的时候瞎划,会导致在屏幕到处乱飞。我也比较进退两难,一时不知道该怎么改了。
图5 控制方式示意图 -
关于上线后的寻找用户的问题。这个小游戏虽说本来就是为了自娱自乐,但既然软件著作权都申请了,不拓展点用户让这个软件活起来则深感可惜。所以,这个小游戏在上完微信之后,几乎就没怎么管了。所以,看到cocos公众号推送消息后,我果断写一篇文章,顺便为自己的小程序引流:)
7.最后
放上一张挑衅的图:),欢迎各路大神前来挑战通关(见图2,二维码),亲测是可以通关的,一共六关,前三关就是完全是送道具。
图6 挑衅:)
也欢迎有想法的朋友继续一起完善下去,有意可以联系我。我原本的设想就是不断慢慢更新关卡。依靠更新关卡的方式提供用户粘性。
整个项目做的比较粗糙,而且有很多设计不合理之处,欢迎各路大神随意喷,不用介意。项目代码写的很粗糙,放出来丢人,就先不放出来,如果确实有阅读量或者有朋友需要参考,到时候再放出来吧。
放一下南天门 那一关的攻略视频,分两卷压缩
cut.part1.rar (2.5 MB)
cut.part2.rar (2.0 MB)
另,游戏内有一个金蟾脱壳的彩蛋,看有没有人能自己发现